Метод управления перечислениями в VBA

Некоторое время меня разочаровывают ограничения, связанные с перечислениями в 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 ответ
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] как деталь реализации.

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

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