Список c# like в VBA

Я хотел бы создать List<T> на VBA, как вы создаете на C#, есть ли способ сделать это? Я искал вопросы об этом здесь, на SO, но я не мог найти ни одного.

3 ответов


дженерики появились в C# 2.0; в VB6/VBA ближе всего вы получаете Collection. Позволяет Add, Remove и Count, но вам нужно будет обернуть его своим собственным классом, если вы хотите больше функциональности, например AddRange, Clear и Contains.

Collection принимает Variant (т. е. все, что вы бросаете на него), поэтому вам придется применять <T> путем проверки типа добавляемых элементов. The TypeName() функция вероятно была бы полезна для этот.


я принял вызов :)

Обновлено посмотреть исходный код здесь

список.cls

добавьте новый модуль класса в проект VB6/VBA. Это определит функциональность List<T> мы реализуем. Как показывает ответ [Santosh], мы немного ограничены в выборе что структура коллекции, которую мы собираемся обернуть. Мы могли бы сделать с массивами, но коллекции, являющиеся объектами, делают лучшего кандидата, так как мы хотим, чтобы перечислитель использовал наш List на For Each строительство.

Безопасность Типа

с List<T> это T говорит этот список-это список того, что именно, и ограничение подразумевает, как только мы определяем тип T этот экземпляр списка прилипает к ней. В VB6 мы можем использовать TypeName чтобы получить строку, представляющую имя типа, с которым мы имеем дело, поэтому мой подход был бы сделать список знаю имя типа, который он держит в тот самый момент, когда добавляется первый элемент: что c# делает декларативно в VB6, мы можем реализовать как вещь времени выполнения. Но это VB6, поэтому давайте не будем сходить с ума по сохранению безопасности типов числовых типов значений - я имею в виду, что мы можем быть умнее, чем VB6 здесь все, что мы хотим, в конце концов, это не код C#; язык не очень жесткий об этом, поэтому компромиссом может быть только разрешить неявное преобразование типов о числовых типах меньшего размера, чем размер первого элемента в списке.

Private Type tList
    Encapsulated As Collection
    ItemTypeName As String
End Type
Private this As tList
Option Explicit

Private Function IsReferenceType() As Boolean
    If this.Encapsulated.Count = 0 Then IsReferenceType = False: Exit Function
    IsReferenceType = IsObject(this.Encapsulated(1))
End Function

Public Property Get NewEnum() As IUnknown
    Attribute NewEnum.VB_Description = "Gets the enumerator from encapsulated collection."
    Attribute NewEnum.VB_UserMemId = -4
    Attribute NewEnum.VB_MemberFlags = "40"

    Set NewEnum = this.Encapsulated.[_NewEnum]
End Property

Private Sub Class_Initialize()
    Set this.Encapsulated = New Collection
End Sub

Private Sub Class_Terminate()
    Set this.Encapsulated = Nothing
End Sub

проверка, если значение соответствующего типа может быть роль функции, которая может быть сделана public для удобства, поэтому значение может быть проверено на действительность клиентским кодом, прежде чем оно будет добавлено. Каждый раз, когда мы инициализируем New List, this.ItemTypeName является пустой строкой для этого экземпляра; в остальное время мы, вероятно, увидим правильный тип, поэтому давайте не будем проверять все возможности (не C#, оценка не сломается при первом Or что соответствует true заявления):

Public Function IsTypeSafe(value As Variant) As Boolean

    Dim result As Boolean
    result = this.ItemTypeName = vbNullString Or this.ItemTypeName = TypeName(value)
    If result Then GoTo QuickExit

    result = result _
        Or this.ItemTypeName = "Integer" And StringMatchesAny(TypeName(value), "Byte") _
        Or this.ItemTypeName = "Long" And StringMatchesAny(TypeName(value), "Integer", "Byte") _
        Or this.ItemTypeName = "Single" And StringMatchesAny(TypeName(value), "Long", "Integer", "Byte") _
        Or this.ItemTypeName = "Double" And StringMatchesAny(TypeName(value), "Long", "Integer", "Byte", "Single") _
        Or this.ItemTypeName = "Currency" And StringMatchesAny(TypeName(value), "Long", "Integer", "Byte", "Single", "Double")

QuickExit:
    IsTypeSafe = result
End Function

теперь это начало.

у нас есть Collection. Это покупает нас Count, Add, Remove и Item. Теперь последнее интересно, потому что это также Collection ' s свойство по умолчанию, и в C# это будет называться индексатор собственность. В VB6 мы устанавливаем 0, и мы получаем свойство по умолчанию:

Public Property Get Item(ByVal index As Long) As Variant
    Attribute Item.VB_Description = "Gets/sets the item at the specified index."
    Attribute Item.VB_UserMemId = 0

    If IsReferenceType Then
        Set Item = this.Encapsulated(index)
    Else
        Item = this.Encapsulated(index)
    End If
End Property

Процедура Атрибутами

в VBA IDE не предоставляет никакого способа их редактирования, но вы можете редактировать код в блокноте и импортировать отредактированный .cls-файл в ваш проект VBA. В VB6 у вас есть меню инструментов для редактирования:

procedure attributesprocedure attributes

Attribute NewEnum.VB_UserMemId = -4 говорит VB использовать это свойство для предоставления перечислителя - мы просто передаем его, что инкапсулированного Collection, и это скрытое свойство начинается с подчеркивания (не пытайтесь сделать это дома!). Attribute NewEnum.VB_MemberFlags = "40" предполагается также сделать его скрытым свойством, но я еще не понял, почему VB не возьмет его. Поэтому, чтобы вызвать геттер для этого скрытого свойства, нам нужно окружить его [] квадратные скобки, потому что идентификатор не может юридически начинаться с подчеркивания в VB6 / VBA.

одна хорошая вещь о (F2) в качестве описания/мини-документации для вашего кода.

Элементы Доступа / "Сеттеры"

в в VB6/VBA в Collection не позволяет напрямую записывать значения в свои элементы. Мы можем назначить ссылки, но не значения. Мы можем реализовать запись с поддержкой List путем предоставления сеттеров для Item собственность - потому что мы не знаю, если наши T будет значением или ссылкой / объектом, мы предоставим оба Let и Set аксессоры. С Collection не поддерживает это, нам придется сначала удалить элемент по указанному индексу, а затем вставить новое значение в этом месте.

хорошие новости RemoveAt и Insert есть два метода, которые нам все равно придется реализовать, и RemoveAt поставляется бесплатно, потому что его семантика такая же, как у инкапсулированных Collection:

Public Sub RemoveAt(ByVal index As Long)
    this.Encapsulated.Remove index
End Sub

Public Sub RemoveRange(ByVal Index As Long, ByVal valuesCount As Long)
    Dim i As Long
    For i = Index To Index + valuesCount - 1
        RemoveAt Index
    Next
End Sub

реализация Insert чувствует, что это может стать намного лучше, но по существу читается как "захватить все после указанный индекс, сделать копию; удалить все после указанного индекса; добавить указанное значение, добавить обратно остальные элементы":

Public Sub Insert(ByVal index As Long, ByVal value As Variant)
    Dim i As Long, isObjRef As Boolean
    Dim tmp As New List

    If index > Count Then Err.Raise 9  'index out of range

    For i = index To Count
        tmp.Add Item(i)
    Next

    For i = index To Count
        RemoveAt index
    Next

    Add value
    Append tmp

End Sub

InsertRange можно взять ParamArray поэтому мы можем поставить рядный значения:

Public Sub InsertRange(ByVal Index As Long, ParamArray values())
    Dim i As Long, isObjRef As Boolean
    Dim tmp As New List

    If Index > Count Then Err.Raise 9  'index out of range

    For i = Index To Count
        tmp.Add Item(i)
    Next

    For i = Index To Count
        RemoveAt Index
    Next

    For i = LBound(values) To UBound(values)
        Add values(i)
    Next
    Append tmp

End Sub

Reverse не имеет ничего общего с сортировкой, поэтому мы можем реализуйте его сразу:

Public Sub Reverse()
    Dim i As Long, tmp As New List

    Do Until Count = 0
        tmp.Add Item(Count)
        RemoveAt Count
    Loop

    Append tmp

End Sub

здесь я подумал, так как VB6 не поддерживает перегрузок. что было бы неплохо иметь метод, который может добавить все элементы из другого списка, так что Append:

Public Sub Append(ByRef values As List)
    Dim value As Variant, i As Long
    For i = 1 To values.Count
        Add values(i)
    Next
End Sub

Add наша List становится больше, чем просто инкапсулируются Collection с несколькими дополнительными методами: если это первый элемент, добавляемый в список, у нас есть часть логики для выполнения здесь-не что мне все равно, сколько элементов есть в инкапсулированной коллекции, поэтому, если все элементы будут удалены из списка, Тип T остается ограниченным:

Public Sub Add(ByVal value As Variant)
    If this.ItemTypeName = vbNullString Then this.ItemTypeName = TypeName(value)
    If Not IsTypeSafe(value) Then Err.Raise 13, ToString, "Type Mismatch. Expected: '" & this.ItemTypeName & "'; '" & TypeName(value) & "' was supplied." 'Type Mismatch
    this.Encapsulated.Add value
End Sub

источник ошибки, вызванной, когда Add сбой является результатом вызова ToString метод, который возвращает... имя типа включая тип T - Итак, мы можем сделать это List<T> вместо List(Of T):

Public Function ToString() As String
    ToString = TypeName(Me) & "<" & Coalesce(this.ItemTypeName, "Variant") & ">"
End Function

List<T> позволяет добавлять много предметы сразу. Сначала я реализовал AddRange с массивом значений для параметра, но затем с использованием мне пришло в голову, что снова, это не C#, и принимая ParamArray гораздо, гораздо удобнее:

Public Sub AddRange(ParamArray values())
    Dim value As Variant, i As Long
    For i = LBound(values) To UBound(values)
        Add values(i)
    Next
End Sub

...И тогда мы доберемся до тех Item сеттеры:

Public Property Let Item(ByVal index As Long, ByVal value As Variant)
    RemoveAt index
    Insert index, value
End Property

Public Property Set Item(ByVal index As Long, ByVal value As Variant)
    RemoveAt index
    Insert index, value
End Property

удаление элемента, предоставляя значение вместо индекса, потребует другого метода, который дает нам индекс этого значения, и потому что мы не только поддерживаем значение типы, но и ссылка типа, это будет очень весело, потому что теперь нам нужен способ определить равенство между ссылочными типами-мы можем получить равенство ссылок сравнение ObjPtr(value), но нам понадобится больше , чем просто это-.net framework научил меня о IComparable и IEquatable. Давайте просто запихнем эти два интерфейса в один и назовем его IComparable - да, вы можете писать и реализовывать интерфейсы в В VB6/VBA В.

интерфейс icomparable.cls

добавьте новый модуль класса и назовите его IComparable - если вы действительно планируете использовать их для чего-то еще, вы можете поместить их в два отдельных модуля класса и вызвать другой IEquatable, но это сделает вас двумя интерфейсами для реализации вместо одного для всех ссылочных типов, с которыми вы хотите работать.

это не макет кода, все, что нужно, это метод подписи:

Option Explicit

Public Function CompareTo(other As Variant) As Integer
'Compares this instance with another; returns one of the following values:
'   -1 if [other] is smaller than this instance.
'    1 if [other] is greater than this instance.
'    0 otherwise.
End Function

Public Function Equals(other As Variant) As Boolean
'Compares this instance with another; returns true if the two instances are equal.
End Function

список.cls

установка интерфейса IComparable для использования

учитывая, что мы упаковали наши IComparable с CompareTo и Equals, теперь мы можем найти индекс любого значения в нашем списке, мы можем также определить, если список содержит любое заданное значение:

Public Function IndexOf(value As Variant) As Long
    Dim i As Long, isRef As Boolean, comparable As IComparable
    isRef = IsReferenceType
    For i = 1 To this.Encapsulated.Count
        If isRef Then
            If TypeOf this.Encapsulated(i) Is IComparable And TypeOf value Is IComparable Then
                Set comparable = this.Encapsulated(i)
                If comparable.Equals(value) Then
                    IndexOf = i
                    Exit Function
                End If
            Else
                'reference type isn't comparable: use reference equality
                If ObjPtr(this.Encapsulated(i)) = ObjPtr(value) Then
                    IndexOf = i
                    Exit Function
                End If
            End If
        Else
            If this.Encapsulated(i) = value Then
                IndexOf = i
                Exit Function
            End If
        End If
    Next
    IndexOf = -1
End Function

Public Function Contains(value As Variant) As Boolean
    Dim v As Variant, isRef As Boolean, comparable As IComparable
    isRef = IsReferenceType
    For Each v In this.Encapsulated
        If isRef Then
            If TypeOf v Is IComparable And TypeOf value Is IComparable Then
                Set comparable = v
                If comparable.Equals(value) Then Contains = True: Exit Function
            Else
                'reference type isn't comparable: use reference equality
                If ObjPtr(v) = ObjPtr(value) Then Contains = True: Exit Function
            End If
        Else
            If v = value Then Contains = True: Exit Function
        End If
    Next
End Function

на CompareTo метод вступает в игру, когда мы начинаем спрашивать, что Min и Max значения могут быть:

Public Function Min() As Variant
    Dim i As Long, isRef As Boolean
    Dim smallest As Variant, isSmaller As Boolean, comparable As IComparable

    isRef = IsReferenceType
    For i = 1 To Count

        If isRef And IsEmpty(smallest) Then
            Set smallest = Item(i)
        ElseIf IsEmpty(smallest) Then
            smallest = Item(i)
        End If

        If TypeOf Item(i) Is IComparable Then
            Set comparable = Item(i)
            isSmaller = comparable.CompareTo(smallest) < 0
        Else
            isSmaller = Item(i) < smallest
        End If

        If isSmaller Then
            If isRef Then
                Set smallest = Item(i)
            Else
                smallest = Item(i)
            End If
        End If
    Next

    If isRef Then
        Set Min = smallest
    Else
        Min = smallest
    End If

End Function

Public Function Max() As Variant
    Dim i As Long, isRef As Boolean
    Dim largest As Variant, isLarger As Boolean, comparable As IComparable

    isRef = IsReferenceType
    For i = 1 To Count

        If isRef And IsEmpty(largest) Then
            Set largest = Item(i)
        ElseIf IsEmpty(largest) Then
            largest = Item(i)
        End If

        If TypeOf Item(i) Is IComparable Then
            Set comparable = Item(i)
            isLarger = comparable.CompareTo(largest) > 0
        Else
            isLarger = Item(i) > largest
        End If

        If isLarger Then
            If isRef Then
                Set largest = Item(i)
            Else
                largest = Item(i)
            End If
        End If
    Next

    If isRef Then
        Set Max = largest
    Else
        Max = largest
    End If

End Function

эти две функции позволяют очень удобочитаемую сортировку-из-за того, что здесь происходит (добавление и удаление элементов), нам придется не быстро:

Public Sub Sort()
    If Not IsNumeric(First) And Not this.ItemTypeName = "String" And Not TypeOf First Is IComparer Then Err.Raise 5, ToString, "Invalid operation: Sort() requires a list of numeric or string values, or a list of objects implementing the IComparer interface."
    Dim i As Long, value As Variant, tmp As New List, minValue As Variant, isRef As Boolean

    isRef = IsReferenceType
    Do Until Count = 0

        If isRef Then
            Set minValue = Min
        Else
            minValue = Min
        End If

        tmp.Add minValue
        RemoveAt IndexOf(minValue)
    Loop

    Append tmp

End Sub

Public Sub SortDescending()
    If Not IsNumeric(First) And Not this.ItemTypeName = "String" And Not TypeOf First Is IComparer Then Err.Raise 5, ToString, "Invalid operation: SortDescending() requires a list of numeric or string values, or a list of objects implementing the IComparer interface."
    Dim i As Long, value As Variant, tmp As New List, maxValue As Variant, isRef As Boolean

    isRef = IsReferenceType
    Do Until Count = 0

        If isRef Then
            Set maxValue = Max
        Else
            maxValue = Max
        End If

        tmp.Add maxValue
        RemoveAt IndexOf(maxValue)
    Loop

    Append tmp

End Sub

последние штрихи

остальное просто тривиальный материал:

Public Sub Remove(value As Variant)
    Dim index As Long
    index = IndexOf(value)
    If index <> -1 Then this.Encapsulated.Remove index
End Sub

Public Property Get Count() As Long
    Count = this.Encapsulated.Count
End Property

Public Sub Clear()
    Do Until Count = 0
        this.Encapsulated.Remove 1
    Loop
End Sub

Public Function First() As Variant
    If Count = 0 Then Exit Function
    If IsObject(Item(1)) Then
        Set First = Item(1)
    Else
        First = Item(1)
    End If
End Function

Public Function Last() As Variant
    If Count = 0 Then Exit Function
    If IsObject(Item(Count)) Then
        Set Last = Item(Count)
    Else
        Last = Item(Count)
    End If
End Function

одна забавная вещь о List<T> заключается в том, что его можно скопировать в массив, просто вызвав ToArray() на это - мы можем сделать точно что:

Public Function ToArray() As Variant()

    Dim result() As Variant
    ReDim result(1 To Count)

    Dim i As Long
    If Count = 0 Then Exit Function

    If IsReferenceType Then
        For i = 1 To Count
            Set result(i) = this.Encapsulated(i)
        Next
    Else
        For i = 1 To Count
            result(i) = this.Encapsulated(i)
        Next
    End If

    ToArray = result
End Function

вот и все!


я использую несколько вспомогательных функций, вот они - они, вероятно, принадлежат в некоторых StringHelpers модуль код:

Public Function StringMatchesAny(ByVal string_source As String, find_strings() As Variant) As Boolean

    Dim find As String, i As Integer, found As Boolean

    For i = LBound(find_strings) To UBound(find_strings)

        find = CStr(find_strings(i))
        found = (string_source = find)

        If found Then Exit For
    Next

    StringMatchesAny = found

End Function

Public Function Coalesce(ByVal value As Variant, Optional ByVal value_when_null As Variant = 0) As Variant

    Dim return_value As Variant
    On Error Resume Next 'supress error handling

    If IsNull(value) Or (TypeName(value) = "String" And value = vbNullString) Then
        return_value = value_when_null
    Else
        return_value = value
    End If

    Err.Clear 'clear any errors that might have occurred
    On Error GoTo 0 'reinstate error handling

    Coalesce = return_value

End Function

и MyClass.cls

эта реализация требует, когда T является ссылочным типом / объектом, который класс реализует IComparable интерфейс для сортировки и поиска индекса значения. Вот как это делается-скажем, у вас есть класс под названием MyClass с числовым или String свойство SomeProperty:

Implements IComparable
Option Explicit

Private Function IComparable_CompareTo(other As Variant) As Integer
    Dim comparable As MyClass
    If Not TypeOf other Is MyClass Then Err.Raise 5

    Set comparable = other
    If comparable Is Nothing Then IComparable_CompareTo = 1: Exit Function

    If Me.SomeProperty < comparable.SomeProperty Then
        IComparable_CompareTo = -1
    ElseIf Me.SomeProperty > comparable.SomeProperty Then
        IComparable_CompareTo = 1
    End If

End Function

Private Function IComparable_Equals(other As Variant) As Boolean
    Dim comparable As MyClass
    If Not TypeOf other Is MyClass Then Err.Raise 5

    Set comparable = other
    IComparable_Equals = comparable.SomeProperty = Me.SomeProperty

End Function

на List можно использовать так:

Dim myList As New List
myList.AddRange 1, 12, 123, 1234, 12345 ', 123456 would blow up because it's a Long
myList.SortDescending

Dim value As Variant
For Each value In myList
   Debug.Print Value
Next

Debug.Print myList.IndexOf(123) 'prints 3
Debug.Print myList.ToString & ".IsTypeSafe(""abc""): " & myList.IsTypeSafe("abc")
    ' prints List<Integer>.IsTypeSafe("abc"): false

List<T> are коллекции который позволяет прикрепить любой тип данных к объекту коллекции, который невозможен в VBA.

индексная коллекция для VBA

коллекция пар ключ-значение для VBA

в качестве альтернативы вы можете создать библиотеку классов в C# и использовать в VBA. Обратитесь к этому ссылке


Я знаю, что это старый пост, но я хотел бы упомянуть следующее В дополнение к тому, что обсуждалось...

Список Выбора

можно использовать ArrayList с, который является слабо типизированным (использует объекты, не сильно типизированные) связанный список, доступный в VBA. Вот пример кода, демонстрирующий базовое использование.

Sub ArrayListDemo()
    Dim MyArray(1 To 7) As String
    MyArray(1) = "A"
    MyArray(2) = "B"
    MyArray(3) = "B"
    MyArray(4) = "i"
    MyArray(5) = "x"
    MyArray(6) = "B"
    MyArray(7) = "C"
    Set L1 = ToList(MyArray)
    L1.Insert L1.LastIndexOf("B"), "Zz"
    Set L2 = L1.Clone
    L2.Sort
    L2.Reverse
    L2.Insert 0, "----------------"
    L2.Insert 0, "Sort and Reverse"
    L2.Insert 0, "----------------"
    L1.AddRange L2.Clone
    Set L3 = SnipArray(L1, 9, 3)
    Debug.Print "---- L1 Values ----"
    For Each obj In L1
        Debug.Print obj & " (L1 & L3 = " & L3.Contains(obj) & ")"
    Next
    Debug.Print "---- L3 Values ----"
    For Each obj In L3
        Debug.Print obj
    Next
End Sub
Function ToList(ByVal Arr As Variant) As Object
    Set ToList = CreateObject("System.Collections.ArrayList")
    For Each Elm In Arr
      ToList.Add Elm
    Next Elm
End Function
Function SnipArray(ByVal ArrayList As Object, lower As Integer, length As Integer) As Object
    Set SnipArray = ArrayList.Clone
    lower = lower - 1
    upper = lower + length
    If upper < ArrayList.Count Then
        SnipArray.RemoveRange upper, (ArrayList.Count - upper)
    End If
    If lower > 0 Then
        SnipArray.RemoveRange 0, lower
    End If
End Function

словарь

кроме того, рад видеть словарь был упомянут. Вот пара заметки о том, как использовать словарь в VBA и использовать его как список:

Sub DictionaryDemo()
    'If you have a reference to "Microsoft Scripting Runtime..."'
    Set D = New Dictionary
    'Else use this if you do not want to bother with adding a reference'
    Set D = CreateObject("Scripting.Dictionary")

    'You can structure a dictionary as a zero based array like this'
    D.Add D.Count, "A"
    Debug.Print D(0)

    Set D = Nothing
End Sub