Некоторое время меня разочаровывают ограничения, связанные с перечислениями в VBA. Гугл не нашел ничего действительно простого и понятного. Итак, немного почесав голову, я придумал следующий код, который предоставляет изящное решение на основе intellisense для управления перечислениями, чтобы обеспечить легкий доступ к
- имена участников
- подсчет членов
- правильное перечисление членов
- проверка, существует ли член перечисления
Код содержится в классе с PredeclaredId, а используемое имя класса — Enums. Большую часть того, что я достиг, можно было бы сделать просто с помощью Scripting.Dictionary, но вы не получите тот intellisense, который предоставляет приведенный ниже код.
Option Explicit
'@PredeclaredId
'@Exposed
Public Enum EnumAction
AsEnum
AsString
AsExists
AsDictionary
AsCount
End Enum
Public Enum TestingEnum
'AsProperty is assigned -1 because it is not included in the backing dictionary
' and we want the enummeration to start at 0 unless defined otherwise
AsProperty = -1
Apples
Oranges
Cars
Lemons
Trees
Giraffes
End Enum
Private Type Enumerations
Testing As Scripting.Dictionary
End Type
Private e As Enumerations
Private Sub Class_Initialize()
If Not Me Is Enums Then
VBA.Err.Raise _
17, _
"Enumerations.ClassInitialize", _
"Class Enums:New'ed Instances of Class Enums are not allowed"
End If
End Sub
Private Sub PopulateTesting()
Set e.Testing = New Scripting.Dictionary
With e.Testing
' Note: AsProperty is not included in the dictionary
.Add Apples, "Apples"
.Add Oranges, "Oranges"
.Add Cars, "Cars"
.Add Lemons, "Lemons"
.Add Trees, "Trees"
.Add Giraffes, "Giraffes"
End With
End Sub
Public Property Get Testing(ByVal ipEnum As TestingEnum, Optional ByVal ipAction As EnumAction = EnumAction.AsEnum) As Variant
If e.Testing Is Nothing Then PopulateTesting
Select Case ipAction
Case EnumAction.AsEnum
Testing = ipEnum
Case EnumAction.AsString
Testing = e.Testing.Item(ipEnum)
Case EnumAction.AsExists
Testing = e.Testing.Exists(ipEnum)
Case EnumAction.AsCount
Testing = e.Testing.Count
Case EnumAction.AsDictionary
Dim myDictionary As Scripting.Dictionary
Set myDictionary = New Scripting.Dictionary
Dim myKey As Variant
For Each myKey In e.Testing
myDictionary.Add myKey, e.Testing.Item(myKey)
Next
Set Testing = myDictionary
End Select
End Property
использование
Public Sub Test()
Const Bannannas As Long = 42
Debug.Print "Enum value of lemons is 3", Enums.Testing(Lemons)
Debug.Print "String is Lemons", Enums.Testing(Lemons, AsString)
Debug.Print "Bannannas are False", Enums.Testing(Bannannas, AsExists)
' The AsProperty member is the preferred awkwardness
' as it is a 'Foreign' member just used to make the
' intellisense a bit more sensible.
' in practise any enumeration member could be used as
' the count and dictionary cases ignore the input enum.
Debug.Print "Count is 6", Enums.Testing(AsProperty, AsCount)
Dim myKey As Variant
Dim myDictionary As Scripting.Dictionary
Set myDictionary = Enums.Testing(AsProperty, AsDictionary)
For Each myKey In myDictionary
Debug.Print myKey, myDictionary.Item(myKey)
Next
Dim mykeys As Variant
mykeys = Enums.Testing(AsProperty, AsDictionary).Keys
Dim myvalues As Variant
myvalues = Enums.Testing(AsProperty, AsDictionary).Items
Debug.Print "Apples are apples", myDictionary.Item(Enums.Testing(Apples))
myDictionary.Item(Enums.Testing(Apples)) = "Plums"
Debug.Print "Apples are plums", myDictionary.Item(Enums.Testing(Apples))
Debug.Print "Apples are apples", Enums.Testing(Apples, AsString)
End Sub
тестовый вывод
Enum value of lemons is 3 3
String is Lemons Lemons
Test is False False
Count is 6 6
0 Apples
1 Oranges
2 Cars
3 Lemons
4 Trees
5 Giraffes
Apples are apples Apples
Apples are plums Plums
Apples are apples Apples
В приведенном выше коде есть некоторые неудобства.
нет поддержки перечислений в качестве значений по умолчанию для необязательных параметров
нет присвоения перечислений константам
Локальная переменная может быть определена с тем же именем, что и член перечисления, но с несуществующим или, что еще хуже, альтернативным значением для члена перечисления
Использование члена «AsProperty» «Foreign» перечисления (частично обрабатывается за счет того, что этот член не включается в резервный скрипт. Словарь.
Буду приветствовать любые комментарии или предложения по улучшению.
1 ответ
Некоторые думали в произвольном порядке:
Может ли Enums
предобъявленный класс будет стандартным модулем? Это позволило бы избежать этой проверки:
Private Sub Class_Initialize() If Not Me Is Enums Then VBA.Err.Raise _ 17, _ "Enumerations.ClassInitialize", _ "Class Enums:New'ed Instances of Class Enums are not allowed" End If End Sub
И сделал бы предписание Enums
по желанию. Обратной стороной будет PopulateTesting
не вызывается автоматически (я полагаю, вы хотели вызвать его в Class_Initialize
), но вы можете вызвать его при первом вызове Public Property Get Testing
что сэкономит попадание во время выполнения, если у вас есть много перечислений для заполнения, но на самом деле требуется только несколько. изменить: в вашем обновленном коде я вижу, что вы выбрали вариант ленивого заполнения
Между прочим, если мы перфекционисты, я бы предпочел видеть этот код, написанный как оговорка о названной защите — а почему магия 17
?
Private Type Enumerations Testing As Scripting.Dictionary End Type Private e As Enumerations
Почему этот уровень модуля? Если я добавлю второе перечисление, зачем ему знать о e.Testing
толковый словарь? Я бы использовал статическую переменную внутри подпрограммы.
Также я бы, наверное, переименовал e.Testing
к this.TestingMap
или даже this.TestingNamesFromEnumValues
.
Существует множество шаблонов, добавляющих новый Enum в этот класс, особенно этот большой блок выбора case:
Select Case ipAction Case EnumAction.AsEnum Testing = ipEnum Case EnumAction.AsString Testing = e.Testing.Item(ipEnum) Case EnumAction.AsExists Testing = e.Testing.Exists(ipEnum) Case EnumAction.AsCount Testing = e.Testing.Count Case EnumAction.AsDictionary Dim myDictionary As Scripting.Dictionary Set myDictionary = New Scripting.Dictionary Dim myKey As Variant For Each myKey In e.Testing myDictionary.Add myKey, e.Testing.Item(myKey) Next Set Testing = myDictionary End Select
… не хотел бы писать это слишком много раз! Это может быть извлечено в частную функцию, которая принимает имя словаря / перечисления в качестве параметра — возможно, класс хранит коллекцию enumName:lookupDictionary
пары, а не жестко кодировать их в UDT.
Материал EnumAction довольно странный, я понимаю, почему вы это сделали, но, честно говоря, эти actions
просто умоляют быть методами класса.
Я думаю, что другой подход здесь состоял бы в том, чтобы иметь 1 класс для каждого перечисления, возможно, даже заранее объявленный и затенявший имя перечисления, хотя строго типизированный член глобальной коллекции мог бы быть более аккуратным API (так что property get Testing() As TestingEnum
). Затем вы можете просто написать несколько вспомогательных функций, позволяющих этим классам где-то регистрировать свои члены, быстро искать их или искать свойства о них, а затем классы перечисления могут использовать их для реализации действий, которые вы хотите, без повторения слишком большого количества шаблонов. Для стандартных методов, таких как Count
или AsDictionary
, ваши объекты Enum могут реализовать стандартный интерфейс, возможно, с простым средством доступа для интерфейса:
Интерфейс: IEnum
Public Property Get Count() As Long
Public Function AsDictionary() As Dictionary
Учебный класс: TestingEnum
Public Property Get Info() As IEnum
Set Info = Me
End Property
тогда ты можешь сделать TestingEnum.Info.Count
Например. Или вызывающий абонент может транслировать на IEnum
и позвони .Count
самих себя. Вы уловили идею.
Scripting.Dictionary не предоставляет IEnumVariant
член, как Коллекции, но вы можете выставить функция генератора чтобы ваши перечисления использовались в каждом цикле
Public Enum TestingEnum 'AsProperty is assigned -1 because it is not included in the backing dictionary ' and we want the enummeration to start at 0 unless defined otherwise AsProperty = -1 Apples Oranges Cars Lemons Trees Giraffes End Enum
Я бы выставил константу из вашего класса, чтобы пользователи знали -1
не является жестким требованием:
Public Const AsPropertyEnumValue As Long = -1 'or anything really
'...
Public Enum TestingEnum
AsProperty = AsPropertyEnumValue
Иметь начальную позицию по умолчанию 0 — это странное требование, я думаю, вам следует отказаться, если пользователь хочет, чтобы яблоки были равны 0, они должны установить его на ноль. Абстрагируясь от реализации этого AsProperty
member будет поощрять пользователя не предполагать какое-либо конкретное начальное значение.
Если вы переключитесь на методы, определенные в интерфейсе, а не в этом параметре Action, то AsProperty
член может быть удален или сделан [_hidden]
как деталь реализации.