User Defined Type. Что это и как его использовать

от автора

Картинка: Designed by vectorjuice / Freepik

Кому будет полезна статья, по мнению автора:
начинающим программистам на языке VBA и тем, кто не работал ранее с оператором Type. Если вы используете этот оператор постоянно, можно сравнить свой вариант применения и вариант автора.

Большинство пользователей VBA прекрасно знают такую штуку как Type, он же User Defined Type (UDT). Кто-то, как я, использует его на повседневной основе. Кто-то, возможно, о нем слышал, но не мог понять как его применить.

Лично я помню, как не так давно смотрел на этот Type и пытался понять зачем он мне нужен, ведь он просто хранит в себе переменные, которые можно с тем же успехом объявить в функции/процедуре или на уровне модуля?

В этой статье я хотел бы показать на примере как можно использовать Type. Мы разберем некоторые его особенности, и возможно кто-нибудь из читателей найдет для себя один из примеров крайне интересным (а может быть даже будет использовать в своих проектах). Поехали!

Вычисляем ошибки, чтобы их не допускать

Что же, для начала давайте обратимся к официальной документации:

(вольный перевод автора)
Оператор Type – используется на уровне модуля для объявления пользовательского типа данных, содержащего один или несколько элементов.Type можно использовать только на уровне модуля. После объявления пользовательского типа вы можете объявить переменную этого типа в любом месте в пределах области видимости. Для объявления переменной пользовательского типа используйте Dim, Private, Public, ReDim или Static… Номера и метки строк не допускаются внутри блоков Type…End Type.

Итак, исходя из документации мы можем выделить два основных момента:

  1. Оператор Type используется только на уровне модуля. Это значит, что его нельзя объявлять в процедурах/функциях/методах/свойствах.

  2. Номера и метки строк не допускаются внутри блоков.

Давайте протестируем оба утверждения:

В первом случае получаем ошибку компиляции «Недопустимая внутренняя процедура»,

ошибка компиляции при объявлении Type внутри процедуры
ошибка компиляции при объявлении Type внутри процедуры

во втором так же ошибка компиляции «Оператор (заявление/утверждение) недопустим внутри блока Type».

ошибка компиляции при объявлении Type с номером/меткой строки внутри блока
ошибка компиляции при объявлении Type с номером/меткой строки внутри блока

Не описано в официальной документации то, что объявленный в Class модуле Type может быть только Private, иначе мы снова получим ошибку компиляции, в этот раз «Нельзя объявлять публичный пользовательский тип в объектном модуле»:

ошибка компиляции при объявлении Public Type в Class модуле
ошибка компиляции при объявлении Public Type в Class модуле

Компилятор перестает ругаться только в случае 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:

обратите внимание, что IDE уже предлагает нам автокомплит данного типа и это прекрасно!
обратите внимание, что IDE уже предлагает нам автокомплит данного типа и это прекрасно!
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, а так же тело письма.

Для начала разделим все наши вводные на несколько блоков:

  1. Блок адресатов: получатель, копия.

  2. Блок письма: тема и тело.

  3. Блок параметров: отправлять или нет, помечать как прочитанное или нет.

Итого получаем три блока по две переменных в каждом.
Как это реализовать? Очень просто.

Для начала, под каждый блок создаем свой 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 присваиваем значения переменнным блока адресатов и блока письма:

параметр передаваемый в функцию CreateLetter остается неизменным, это важно
параметр передаваемый в функцию CreateLetter остается неизменным, это важно
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? Пишите в комментариях!
А так же, подписывайтесь на мой
телеграмм.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Умели раньше пользоваться оператором Type?
25% Да, использую его так же как автор 1
25% Да, но использую его иначе (опишите, пожалуйста, как именно в комментариях) 1
0% Нет, но слышал о нем. Не вижу смысла применять 0
25% Нет, но слышал о нем. Возможно теперь буду применять 1
0% Нет, и не слышал. Не вижу смысла применять 0
25% Нет, и не слышал. Возможно теперь буду применять 1
Проголосовали 4 пользователя. Воздержавшихся нет.

ссылка на оригинал статьи https://habr.com/ru/post/691000/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *