Картинка: Designed by vectorjuice / Freepik
Кому будет полезна статья, по мнению автора:
начинающим программистам на языке VBA и тем, кто не работал ранее с оператором Type. Если вы используете этот оператор постоянно, можно сравнить свой вариант применения и вариант автора.
Большинство пользователей VBA прекрасно знают такую штуку как Type
, он же User Defined Type (UDT). Кто-то, как я, использует его на повседневной основе. Кто-то, возможно, о нем слышал, но не мог понять как его применить.
Лично я помню, как не так давно смотрел на этот Type
и пытался понять зачем он мне нужен, ведь он просто хранит в себе переменные, которые можно с тем же успехом объявить в функции/процедуре или на уровне модуля?
В этой статье я хотел бы показать на примере как можно использовать Type
. Мы разберем некоторые его особенности, и возможно кто-нибудь из читателей найдет для себя один из примеров крайне интересным (а может быть даже будет использовать в своих проектах). Поехали!
Вычисляем ошибки, чтобы их не допускать
Что же, для начала давайте обратимся к официальной документации:
(вольный перевод автора)
Оператор Type – используется на уровне модуля для объявления пользовательского типа данных, содержащего один или несколько элементов.Type можно использовать только на уровне модуля. После объявления пользовательского типа вы можете объявить переменную этого типа в любом месте в пределах области видимости. Для объявления переменной пользовательского типа используйте Dim, Private, Public, ReDim или Static... Номера и метки строк не допускаются внутри блоков Type...End Type.
Итак, исходя из документации мы можем выделить два основных момента:
Оператор Type используется только на уровне модуля. Это значит, что его нельзя объявлять в процедурах/функциях/методах/свойствах.
Номера и метки строк не допускаются внутри блоков.
Давайте протестируем оба утверждения:
В первом случае получаем ошибку компиляции "Недопустимая внутренняя процедура",
во втором так же ошибка компиляции "Оператор (заявление/утверждение) недопустим внутри блока Type".
Не описано в официальной документации то, что объявленный в Class
модуле Type
может быть только Private
, иначе мы снова получим ошибку компиляции, в этот раз "Нельзя объявлять публичный пользовательский тип в объектном модуле":
Компилятор перестает ругаться только в случае Private Type
в Class
модуле, но здесь нужно помнить, что возвращать такой UDT можно только Private
функцией, иначе:
мы снова получим ошибку компиляции, теперь это "Private перечисления и пользовательские типы, не могут использоваться в качестве параметров или возвращаемых типов для Public процедур, членов данных или полей пользовательских типов".
Кстати, как и обозначено в описании ошибки, в модуле класса нельзя создавать публичные поля или использовать параметры для публичных методов с приватным типом UDT. Ну оно и логично.
Постановка задачи
Итак, если я не ошибаюсь, с ошибками мы разобрались. Перейдем к использованию.
Давайте представим, что наша задача – почтовая рассылка по определенному скрипту. Во время выполнения макроса мы получаем информацию об email-адресе получателя, адресате копии письма и его теме, после чего все эти данные нам нужно передать в отдельную функцию, которая занимается созданием письма и его отправкой или сохранением в черновики.
Решаем без UDT
Для начала разберемся с обычным модулем. Про использование UDT в Class
модуле я напишу отдельную статью.
Как можно решить эту задачу стандартными средствами?
Что ж, первое что мы делаем – объявляем переменные, которые будут содержать адрес получателя и адрес адресата копии (простите за тавтологию), а так же тему письма, после чего присваиваем напрямую значения, чтобы не усложнять пример, и отправляем их как аргументы в функцию CreateLetter
:
Sub Mailing()
Dim AddressTo As String: AddressTo = "exampleTo@test.vba"
Dim AddressCC As String: AddressCC = "exampleCC@test.vba"
Dim Subject As String: Subject = "Тема письма"
CreateLetter AddressTo, AddressCC, Subject
End Sub
Далее, пропишем функцию, которая создаст и отправит или сохранит письмо (это значение сделаем необязательным, по умолчанию установим в False
):
Sub CreateLetter(ByVal AddressTo As String, _
ByVal AddressCC As String, _
ByVal Subject As String, _
Optional ByVal Submit As Boolean = False)
Dim Outlook As Object
Set Outlook = CreateObject("Outlook.Application")
With Outlook.CreateItem(olMailItem)
.To = AddressTo
.CC = AddressCC
.Subject = Subject
If Submit Then .Send
End With
End Sub
Итак, в целом все нормально. У нас есть данные, мы передаем их в функцию, функция их использует.
Но это всего лишь два адреса и тема.
А теперь представим, что нам нужно передавать еще текст тела письма и вложение.
А еще в параметрах можно указать нужно ли удалять письмо после отправки (свойство DeleteAfterSubmit
), или указать нужно ли отметить неотправленное письмо (черновик) как прочитанное (свойство UnRead
).
А еще, возможно нам потребуется создавать письмо из другой процедуры и тогда снова придется перечислять все переменные в объявлении и передавать их все в функцию.
И многое, многое другое...
Представьте на секунду насколько сильно разрастутся параметры функции.
Плюс, копия в письме может быть не всегда, как и вложение. Тогда придется делать все параметры Optional
? Или прописать ParamArray
? Это все не наглядно и может вызвать ошибки, в случае не верной передачи параметров.
Код становится менее читаемым и сумбурным, согласитесь. На таком небольшом примере все ок, ничего особо критичного. Но в реальном проекте это может стать большой проблемой.
Гораздо более лаконичное решение, как вы уже поняли, использовать UDT.
Решаем с UDT
Для решения нам потребуется объявить Type
на уровне модуля и поместить в него все наши переменные. Давайте назовем его TLetter
:
Type TLetter
AddressTo As String
AddressCC As String
Subject As String
End Type
Далее, в процедуре Mailing
создадим переменную Letter
типа TLetter
:
Sub Mailing()
Dim Letter As TLetter
Dim AddressTo As String: AddressTo = "exampleTo@test.vba"
Dim AddressCC As String: AddressCC = "exampleCC@test.vba"
Dim Subject As String: Subject = "Тема письма"
CreateLetter AddressTo, AddressCC, Subject
End Sub
Теперь, всем полям нашего типа присваиваем необходимые значения. Сделать это можно написав имя переменной Letters
и далее через точку выбрать нужное поле:
Sub Mailing()
Dim Letter As TLetter
Letter.AddressTo = "exampleTo@test.vba"
Letter.AddressCC = "exampleCC@test.vba"
Letter.Subject = "Тема письма"
CreateLetter Letter
End Sub
Ничего вам это не напоминает?????
Если вы сказали "да это же как объект" – то вы совершенно правы. Взаимодействие с Type очень похоже на взаимодействие с объектами. Только мы объявляем его без ключевых слов New и Set, как в случае с объектами, а так же не сможем поместить в него функции/процедуры. Я бы даже назвал этот блок, скорее, своего рода, структурой.
Все что нам осталось сделать – заменить в процедуре CreateLetter
три старых параметра на один новый и переписать присваивание параметров:
Sub CreateLetter(ByRef Letter As TLetter, _
Optional ByVal Submit As Boolean = False)
Dim Outlook As Object
Set Outlook = CreateObject("Outlook.Application")
With Outlook.CreateItem(olMailItem)
.To = Letter.AddressTo
.CC = Letter.AddressCC
.Subject = Letter.Subject
If Submit Then .Send
End With
End Sub
Кстати, в блоке Ошибки я забыл упомянуть еще одну небольшую особенность – UDT в параметры можно передавать только
ByRef
.
Так лучше, верно?
Не совсем. Давайте уберем последний опциональный параметр Submit
из функции и пропишем его в нашей структуре как поле:
Option Explicit
Type TLetter
AddressTo As String
AddressCC As String
Subject As String
Submit As Boolean ' Переносим параметр в структуру.
End Type
Sub Mailing()
Dim Letter As TLetter
Letter.AddressTo = "exampleTo@test.vba"
Letter.AddressCC = "exampleCC@test.vba"
Letter.Subject = "Тема письма"
CreateLetter Letter
End Sub
Sub CreateLetter(ByRef Letter As TLetter)
Dim Outlook As Object
Set Outlook = CreateObject("Outlook.Application")
With Outlook.CreateItem(olMailItem)
.To = Letter.AddressTo
.CC = Letter.AddressCC
.Subject = Letter.Subject
If Letter.Submit Then .Send ' Передаем поле из структуры.
End With
End Sub
Вот теперь действительно лучше.
Обратите внимание, мы не присваиваем полю Submit
значение в процедуре Mailing
. Не присвоенное значение по умолчанию останется False
:
Думаю не нужно объяснять, что расширять этот тип можно сколько угодно, при этом использовать все его поля нет необходимости. Вы можете оставлять их пустыми и уже в функции прописывать валидацию для пустых полей, если в этом есть потребность.
Расширяем возможности
Итак, мы научились складывать несколько связанных переменных в одну и использовать их в качестве аргумента функции.
Но что если нам нужно очень много таких переменных и при этом они связаны между собой в своего рода блоки, а эти блоки можно связать в одну единую переменную?
Давайте добавим новые вводные в задачу и рассмотрим на примере.
Допустим в функцию CreateLetter
нам нужно дополнительно передавать параметр UnRead
, а так же тело письма.
Для начала разделим все наши вводные на несколько блоков:
Блок адресатов: получатель, копия.
Блок письма: тема и тело.
Блок параметров: отправлять или нет, помечать как прочитанное или нет.
Итого получаем три блока по две переменных в каждом.
Как это реализовать? Очень просто.
Для начала, под каждый блок создаем свой UDT:
Option Explicit
' Блок адресатов
Type TRecipient
To As String
CC As String
End Type
' Блок письма
Type TMain
Subject As String
Body As String
End Type
' Блок параметров
Type TParameter
Submit As Boolean
UnRead As Boolean
End Type
После чего снова создаем UDT TLetter
, а уже в нем объявляем три переменных с ранее созданными блоками:
Type TLetter
Recipient As TRecipient
Main As TMain
Parameter As TParameter
End Type
Да, так можно было. ????
Дальше, что называется, следите за руками.
В процедуре Mailing
через уже знакомую переменную Letter
присваиваем значения переменнным блока адресатов и блока письма:
Sub Mailing()
Dim Letter As TLetter
Letter.Recipient.To = "exampleTo@test.vba"
Letter.Recipient.CC = "exampleCC@test.vba"
Letter.Main.Subject = "Тема письма"
Letter.Main.Body = "Тело письма"
CreateLetter Letter
End Sub
Немного корректируем функцию CreateLetter
и добавляем новые параметры для создаваемого элемента письма (не функции):
Sub CreateLetter(ByRef Letter As TLetter)
Dim Outlook As Object
Set Outlook = CreateObject("Outlook.Application")
With Outlook.CreateItem(olMailItem)
.To = Letter.Recipient.To
.CC = Letter.Recipient.CC
.Subject = Letter.Main.Subject
.Body = Letter.Main.Body
.UnRead = Letter.Parameter.UnRead
If Letter.Parameter.Submit Then .Send
End With
End Sub
И все! Да, так просто.
В реальных задачах меня такая гибкость очень сильно выручала, выручает и, уверен, еще будет выручать.
Что в итоге
В итоге, мы имеем очень удобный и гибкий инструмент для хранения некой связанной структуры данных.
Так же, этот инструмент помогает нам защитить код от ошибок на моменте его написания, потому что передать непонятно что в функцию будет сильно сложнее, чем если бы мы использовали обычные типы.
Код, благодаря такому подходу, становится, во-первых, более читаемым, и во-вторых, более гибким и расширяемым. С таким кодом гораздо приятнее работать.
А ведь это важные вещи, к которым мы все стремимся при написании кода.
Это не все, что я хотел рассказать про Type
. В следующей статье рассмотрим еще один пример использования UDT в модуле, а так же увидим как его применять в Class
модуле.
Спасибо, что прочитали до конца.
А как вы используете Type? Пишите в комментариях!
А так же, подписывайтесь на мой телеграмм.
navferty
Для меня самым чувствительным недостатком этой фичи является то, что UDT нельзя присваивать в variant переменную, и как следствие - нельзя использовать в словарях и коллекциях (а можно только в массиве, что менее удобно при динамичесом добавлении/удалении элементов).
Я так понимаю, что это ограничение обусловлено переменным размером UDT в памяти, и так как это значимый тип, его нельзя запихнуть в переменную Variant фиксированного размера (16 байт вроде). Знающие люди, подскажите - есть ли обходной путь (например что-то вроде аналога boxing из C#)?
ArtCapCorn Автор
Насколько мне известно, увы да, udt нельзя добавить в collection или в dictionary.
Только массив, Вы правы.
Как альтернатива - создать класс, аналогично udt. В данном случае будет более гибкая модель.
IvanSTV
именно поэтому просто создаю классы - удобней во всех отношениях. . Читал статью и все думал - в чем преимущество перед классом? И не увидел.
ArtCapCorn Автор
Ну, на самом деле, с такими мыслями можно и от ООП отказаться в пользу ФП, а вместо Enum пользоваться константами. Да и они не нужны, просто в процедуре присваивать значения, да и все :)
Есть у меня один коллега, который достаточно давно программирует на VBA, но не пользуется классами. Коллеги решили ему показать пример, что классы могут быть вполне удобны в использовании, и даже удобнее, чем просто функции юзать. Но человека переубедить невозможно)
Ну и мое сугубо личное мнение - если в языке есть функционал, надо его как минимум знать, а в идеале использовать (не просто же так его завезли). Могу быть не прав, но на то это и сугубо личное мнение.
Ни в коем случае не камень, и не в Ваш огород. Просто размышления в слух.
P.S. Кстати, в статье я не пытаюсь сравнить Type с Классом (в плане «что лучше, то или это»). А если создается такое ощущение, то оно ложное, отбросьте его :)