Ошибка с перечислением для каждого в пользовательских классах x64

Несколько месяцев назад я обнаружил ошибку в VBA и не смог найти достойного решения. Ошибка действительно раздражает, так как она как бы ограничивает приятную языковую функцию.

При использовании Custom Collection Class довольно часто требуется иметь перечислитель, чтобы этот класс можно было использовать в For Eachпетля. Это можно сделать, добавив эту строку:

       Attribute [MethodName].VB_UserMemId = -4 'The reserved DISPID_NEWENUM

сразу после строки подписи функции / свойства либо:

  1. Экспорт модуля класса, редактирование содержимого в текстовом редакторе и последующий импорт обратно
  2. Использование аннотации Rubberduck'@Enumerator над сигнатурой функции, а затем синхронизируйте

К сожалению, на x64 использование вышеупомянутой функции приводит к записи неправильной памяти и в некоторых случаях приводит к сбою приложения (обсуждается позже).

Воспроизведение ошибки

CustomCollection класс:

       VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "CustomCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Private m_coll As Collection

Private Sub Class_Initialize()
    Set m_coll = New Collection
End Sub
Private Sub Class_Terminate()
    Set m_coll = Nothing
End Sub

Public Sub Add(v As Variant)
    m_coll.Add v
End Sub

Public Function NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
    Set NewEnum = m_coll.[_NewEnum]
End Function

Код в стандартном модуле:

       Option Explicit

Sub Main()
    #If Win64 Then
        Dim c As New CustomCollection
        c.Add 1
        c.Add 2
        ShowBug c
    #Else
        MsgBox "This bug does not occur on 32 bits!", vbInformation, "Cancelled"
    #End If
End Sub

Sub ShowBug(c As CustomCollection)
    Dim ptr0 As LongPtr
    Dim ptr1 As LongPtr
    Dim ptr2 As LongPtr
    Dim ptr3 As LongPtr
    Dim ptr4 As LongPtr
    Dim ptr5 As LongPtr
    Dim ptr6 As LongPtr
    Dim ptr7 As LongPtr
    Dim ptr8 As LongPtr
    Dim ptr9 As LongPtr
    '
    Dim v As Variant
    '
    For Each v In c
    Next v
    Debug.Assert ptr0 = 0
End Sub

Запустив Main метод, код остановится на Assert линия в ShowBugметод, и вы можете увидеть в Locals окне, что локальные переменные получили их ценности изменились из ниоткуда: где ptr1 равно

ObjPtr(c). Чем больше переменных используется внутри NewEnum (включая необязательные параметры), тем больше ptrs в ShowBug метод записывается со значением (адресом памяти).

Излишне говорить, что удаление локальных переменных ptr внутри ShowBug метод наверняка вызовет сбой приложения.

При пошаговом выполнении кода построчно эта ошибка не возникает!


Подробнее об ошибке

Ошибка не связана с фактическим Collection хранится внутри CustomCollection. Память записывается сразу после вызова функции NewEnum. Итак, выполнение любого из следующих действий не помогает (проверено):

  1. добавление Optional параметры
  2. удаление всего кода из функции (см. ниже код, показывающий это)
  3. объявив как IUnknown вместо IEnumVariant
  4. вместо Function объявив как Property Get
  5. используя такие ключевые слова, как Friend или же Static в сигнатуре метода
  6. добавление DISPID_NEWENUM к Let или Set аналогу Get или даже сокрытие первого (т.е. сделать Let/Set закрытым).

Давайте попробуем шаг 2, упомянутый выше. Если CustomCollection становится:

       VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "CustomCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Public Function NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
End Function

и код, используемый для тестирования, изменен на:

       Sub Main()
    #If Win64 Then
        Dim c As New CustomCollection
        ShowBug c
    #Else
        MsgBox "This bug does not occur on 32 bits!", vbInformation, "Cancelled"
    #End If
End Sub

Sub ShowBug(c As CustomCollection)
    Dim ptr0 As LongPtr
    Dim ptr1 As LongPtr
    Dim ptr2 As LongPtr
    Dim ptr3 As LongPtr
    Dim ptr4 As LongPtr
    Dim ptr5 As LongPtr
    Dim ptr6 As LongPtr
    Dim ptr7 As LongPtr
    Dim ptr8 As LongPtr
    Dim ptr9 As LongPtr
    '
    Dim v As Variant
    '
    On Error Resume Next
    For Each v In c
    Next v
    On Error GoTo 0
    Debug.Assert ptr0 = 0
End Sub

Бег Main выдает ту же ошибку.

Обходной путь

Надежные способы, которые я нашел, чтобы избежать ошибки:

  1. Вызвать метод (в основном оставить ShowBugметод) и вернитесь. Это должно произойти до For Eachстрока выполняется (до значения, что она может быть где угодно в том же методе, не обязательно в точной строке до):

             Sin 0 'Or VBA.Int 1 - you get the idea
    For Each v In c
    Next v
    

    Минусы: легко забыть

  2. Сделать Setзаявление. Это может быть вариант, используемый в цикле (если не используются другие объекты). Как и в пункте 1 выше, это должно произойти до For Each строка выполняется:

             Set v = Nothing
    For Each v In c
    Next v
    

    или даже установив коллекцию для себя с помощью Set c = c
    Или, передав параметр cByVal к ShowBugметод (который, как и Set, вызывает IUnknonw::AddRef)
    Минусы: легко забыть

  3. Использование отдельного EnumHelper class, который является единственным классом, когда-либо используемым для перечисления:

             VERSION 1.0 CLASS
    BEGIN
      MultiUse = -1  'True
    END
    Attribute VB_Name = "EnumHelper"
    Attribute VB_GlobalNameSpace = False
    Attribute VB_Creatable = False
    Attribute VB_PredeclaredId = False
    Attribute VB_Exposed = False
    Option Explicit
    
    Private m_enum As IEnumVARIANT
    
    Public Property Set EnumVariant(newEnum_ As IEnumVARIANT)
        Set m_enum = newEnum_
    End Property
    Public Property Get EnumVariant() As IEnumVARIANT
    Attribute EnumVariant.VB_UserMemId = -4
        Set EnumVariant = m_enum
    End Property
    

    CustomCollection станет:

             VERSION 1.0 CLASS
    BEGIN
      MultiUse = -1  'True
    END
    Attribute VB_Name = "CustomCollection"
    Attribute VB_GlobalNameSpace = False
    Attribute VB_Creatable = False
    Attribute VB_PredeclaredId = False
    Attribute VB_Exposed = False
    Option Explicit
    
    Private m_coll As Collection
    
    Private Sub Class_Initialize()
        Set m_coll = New Collection
    End Sub
    Private Sub Class_Terminate()
        Set m_coll = Nothing
    End Sub
    
    Public Sub Add(v As Variant)
        m_coll.Add v
    End Sub
    
    Public Function NewEnum() As EnumHelper
        Dim eHelper As New EnumHelper
        '
        Set eHelper.EnumVariant = m_coll.[_NewEnum]
        Set NewEnum = eHelper
    End Function
    

    и код вызова:

             Option Explicit
    
    Sub Main()
        #If Win64 Then
            Dim c As New CustomCollection
            c.Add 1
            c.Add 2
            ShowBug c
        #Else
            MsgBox "This bug does not occur on 32 bits!", vbInformation, "Cancelled"
        #End If
    End Sub
    
    Sub ShowBug(c As CustomCollection)
        Dim ptr0 As LongPtr
        Dim ptr1 As LongPtr
        Dim ptr2 As LongPtr
        Dim ptr3 As LongPtr
        Dim ptr4 As LongPtr
        Dim ptr5 As LongPtr
        Dim ptr6 As LongPtr
        Dim ptr7 As LongPtr
        Dim ptr8 As LongPtr
        Dim ptr9 As LongPtr
        '
        Dim v As Variant
        '
        For Each v In c.NewEnum
            Debug.Print v
        Next v
        Debug.Assert ptr0 = 0
    End Sub
    

    Очевидно, что зарезервированный DISPID был удален из CustomCollection класс.

    Плюсы: принуждение For Each на .NewEnumвместо настраиваемой коллекции напрямую. Это позволяет избежать сбоев, вызванных ошибкой.

    Минусы: всегда нужно дополнительное EnumHelperкласс. Легко забыть добавить .NewEnum в For Each строка (вызовет только ошибку времени выполнения).

Последний подход (3) работает, потому что когда c.NewEnum выполняется ShowBug метод завершается, а затем возвращается перед вызовом Property Get EnumVariant внутри EnumHelperкласс. В основном подход (1) позволяет избежать ошибки.


Чем объясняется такое поведение? Можно ли избежать этой ошибки более элегантным способом?

РЕДАКТИРОВАТЬ

Прохождение CustomCollectionByVal - не всегда вариант. Рассмотрим Class1:

       Option Explicit

Private m_collection As CustomCollection

Private Sub Class_Initialize()
    Set m_collection = New CustomCollection
End Sub
Private Sub Class_Terminate()
    Set m_collection = Nothing
End Sub

Public Sub AddElem(d As Double)
    m_collection.Add d
End Sub

Public Function SumElements() As Double
    Dim v As Variant
    Dim s As Double
    
    For Each v In m_collection
        s = s + v
    Next v
    SumElements = s
End Function

А теперь обзвон:

       Sub ForceBug()
    Dim c As Class1
    Set c = New Class1
    c.AddElem 2
    c.AddElem 5
    c.AddElem 7
    
    Debug.Print c.SumElements 'BOOM - Application crashes
End Sub

Очевидно, что этот пример немного натянут, но довольно часто бывает, что "родительский" объект содержит настраиваемую коллекцию "дочерних" объектов, и "родительский" может захотеть выполнить некоторую операцию с участием некоторых или всех "дочерних".

В этом случае было бы легко забыть сделать Set оператор или вызов метода перед For Each линия.

2 ответа

Решение

Что происходит

Похоже, что кадры стека перекрываются, хотя этого не должно быть. Наличие достаточного количества переменных в методе предотвращает сбой, а значения переменных (в вызывающей подпрограмме) просто изменяются, потому что память, на которую они ссылаются, также используется другим фреймом стека (вызываемой подпрограммой), который был добавлен / передан позже в вершина стека вызовов.

Мы можем проверить это, добавив пару Debug.Print утверждения к тому же коду из вопроса.

Класс:

      VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "CustomCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Private m_coll As Collection

Private Sub Class_Initialize()
    Set m_coll = New Collection
End Sub
Private Sub Class_Terminate()
    Set m_coll = Nothing
End Sub

Public Sub Add(v As Variant)
    m_coll.Add v
End Sub

Public Function NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
    Debug.Print "The NewEnum return address " & VarPtr(NewEnum) & " should be outside of the"
    Set NewEnum = m_coll.[_NewEnum]
End Function

И вызывающий код в стандартном модуле .bas:

      Option Explicit

Sub Main()
    #If Win64 Then
        Dim c As New CustomCollection
        c.Add 1
        c.Add 2
        ShowBug c
    #Else
        MsgBox "This bug does not occur on 32 bits!", vbInformation, "Cancelled"
    #End If
End Sub

Sub ShowBug(ByRef c As CustomCollection)
    Dim ptr0 As LongPtr
    Dim ptr1 As LongPtr
    Dim ptr2 As LongPtr
    Dim ptr3 As LongPtr
    Dim ptr4 As LongPtr
    Dim ptr5 As LongPtr
    Dim ptr6 As LongPtr
    Dim ptr7 As LongPtr
    Dim ptr8 As LongPtr
    Dim ptr9 As LongPtr
    '
    Dim v As Variant
    '
    For Each v In c
    Next v
    Debug.Print VarPtr(ptr9) & " - " & VarPtr(ptr0) & " memory range"
    Debug.Assert ptr0 = 0
End Sub

Запустив Main В окне Immediate Window я получаю что-то вроде этого:

Адрес возвращаемого значения явно находится в адресе памяти между переменными и метода. Вот почему переменные получают значения из ниоткуда, потому что они фактически берутся из кадра стека метода (например, адрес vtable объекта или адрес IEnumVariantинтерфейс). Если бы переменных не было, тогда сбой очевиден, поскольку перезаписываются более важные части памяти (например, адрес указателя кадра для метода). Поскольку кадр стека для метода больше (мы можем добавить локальные переменные, например, для увеличения размера), тем больше памяти распределяется между верхним кадром стека и тем, который находится ниже в стеке вызовов.

Что произойдет, если мы исправим ошибку с помощью параметров, описанных в вопросе? Просто добавив перед строкой, вы получите:

Отображая как предыдущее значение, так и текущее (с синей рамкой), мы видим, что возврат находится по адресу памяти за пределами ptr0 а также ptr9переменные метода. Кажется, что кадр стека был правильно выделен с помощью обходного пути.

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

Как вызывает

Каждый класс VBA является производным от IDispatch (который, в свою очередь, является производным от IUnknown).

Когда для объекта вызывается цикл, метод этого объекта вызывается с dispIDMemberравно -4. В VBA.Collection уже есть такой член, но для пользовательских классов VBA мы помечаем наш собственный метод с помощью Attribute NewEnum.VB_UserMemId = -4 чтобы Invoke мог вызвать наш метод.

не вызывается напрямую, если интерфейс, используемый в строке, не является производным от IDispatch. Вместо этого сначала вызывается и запрашивается интерфейс IDispatch. В этом случае, очевидно, вызывается только после возврата интерфейса IDispatch. Вот причина, по которой использование объекта, объявленного As IUnknownне вызовет ошибку, независимо от того, передана она или является глобальной пользовательской коллекцией или членом класса. Он просто использует обходной путь номер 1, упомянутый в вопросе (т.е. вызывает другой метод), хотя мы его не видим.

Привязка вызова

Мы можем заменить метод, не связанный с VB, одним из наших, чтобы продолжить исследование. В стандарте .bas модуль нам понадобится следующий код для перехвата:

      Option Explicit

#If Mac Then
    #If VBA7 Then
        Private Declare PtrSafe Function CopyMemory Lib "/usr/lib/libc.dylib" Alias "memmove" (Destination As Any, Source As Any, ByVal Length As LongPtr) As LongPtr
    #Else
        Private Declare Function CopyMemory Lib "/usr/lib/libc.dylib" Alias "memmove" (Destination As Any, Source As Any, ByVal Length As Long) As Long
    #End If
#Else 'Windows
    'https://msdn.microsoft.com/en-us/library/mt723419(v=vs.85).aspx
    #If VBA7 Then
        Public Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
    #Else
        Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
    #End If
#End If

#If Win64 Then
    Private Const PTR_SIZE As Long = 8
#Else
    Private Const PTR_SIZE As Long = 4
#End If

#If VBA7 Then
    Private newInvokePtr As LongPtr
    Private oldInvokePtr As LongPtr
    Private invokeVtblPtr As LongPtr
#Else
    Private newInvokePtr As Long
    Private oldInvokePtr As Long
    Private invokeVtblPtr As Long
#End If

'https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-idispatch-invoke
Function IDispatch_Invoke(ByVal this As Object _
    , ByVal dispIDMember As Long _
    , ByVal riid As LongPtr _
    , ByVal lcid As Long _
    , ByVal wFlags As Integer _
    , ByVal pDispParams As LongPtr _
    , ByVal pVarResult As LongPtr _
    , ByVal pExcepInfo As LongPtr _
    , ByRef puArgErr As Long _
) As Long
    Const DISP_E_MEMBERNOTFOUND = &H80020003
    '
    Debug.Print "The IDispatch::Invoke return address " & VarPtr(IDispatch_Invoke) & " should be outside of the"
    IDispatch_Invoke = DISP_E_MEMBERNOTFOUND
End Function

Sub HookInvoke(obj As Object)
    If obj Is Nothing Then Exit Sub
    #If VBA7 Then
        Dim vTablePtr As LongPtr
    #Else
        Dim vTablePtr As Long
    #End If
    '
    newInvokePtr = VBA.Int(AddressOf IDispatch_Invoke)
    CopyMemory vTablePtr, ByVal ObjPtr(obj), PTR_SIZE
    '
    invokeVtblPtr = vTablePtr + 6 * PTR_SIZE
    CopyMemory oldInvokePtr, ByVal invokeVtblPtr, PTR_SIZE
    CopyMemory ByVal invokeVtblPtr, newInvokePtr, PTR_SIZE
End Sub

Sub RestoreInvoke()
    If invokeVtblPtr = 0 Then Exit Sub
    '
    CopyMemory ByVal invokeVtblPtr, oldInvokePtr, PTR_SIZE
    invokeVtblPtr = 0
    oldInvokePtr = 0
    newInvokePtr = 0
End Sub

и мы запускаем метод (стандартный модуль .bas), чтобы выдать ошибку:

      Option Explicit

Sub Main2()
    #If Win64 Then
        Dim c As Object
        Set c = New CustomCollection
        c.Add 1
        c.Add 2
        '
        HookInvoke c
        ShowBug2 c
        RestoreInvoke
    #Else
        MsgBox "This bug does not occur on 32 bits!", vbInformation, "Cancelled"
    #End If
End Sub

Sub ShowBug2(ByRef c As CustomCollection)
    Dim ptr00 As LongPtr
    Dim ptr01 As LongPtr
    Dim ptr02 As LongPtr
    Dim ptr03 As LongPtr
    Dim ptr04 As LongPtr
    Dim ptr05 As LongPtr
    Dim ptr06 As LongPtr
    Dim ptr07 As LongPtr
    Dim ptr08 As LongPtr
    Dim ptr09 As LongPtr
    Dim ptr10 As LongPtr
    Dim ptr11 As LongPtr
    Dim ptr12 As LongPtr
    Dim ptr13 As LongPtr
    Dim ptr14 As LongPtr
    Dim ptr15 As LongPtr
    Dim ptr16 As LongPtr
    Dim ptr17 As LongPtr
    Dim ptr18 As LongPtr
    Dim ptr19 As LongPtr
    '
    Dim v As Variant
    '
    On Error Resume Next
    For Each v In c
    Next v
    Debug.Print VarPtr(ptr19) & " - " & VarPtr(ptr00) & " range on the call stack"
    Debug.Assert ptr00 = 0
End Sub

Обратите внимание, что для предотвращения сбоя требуется больше фиктивных переменных ptr, поскольку кадр стека для большего размера (следовательно, больше перекрытие памяти).

Выполнив вышеуказанное, я получаю:

Такая же ошибка возникает, хотя код никогда не достигает метода из-за перехвата метода. Фрейм стека снова размещен неправильно.

Опять же, добавив Set v = Nothing перед For Each v In c приводит к:

Кадр стека размещен правильно (с зеленой рамкой). Это указывает на то, что проблема не в методе, а также не в нашем методе замены. Что-то происходит до того, как наш будет вызван.

Если мы сломаемся внутри нашего IDispatch_Invoke стек вызовов выглядит так:

И последний пример. Рассмотрим пустой (без кода) класс Class1. Если мы запустим следующий код:

      Option Explicit

Sub Main3()
    #If Win64 Then
        Dim c As New Class1
        ShowBug3 c
    #Else
        MsgBox "This bug does not occur on 32 bits!", vbInformation, "Cancelled"
    #End If
End Sub

Sub ShowBug3(ByRef c As Class1)
    Dim ptr0 As LongPtr
    Dim ptr1 As LongPtr
    Dim ptr2 As LongPtr
    Dim ptr3 As LongPtr
    Dim ptr4 As LongPtr
    Dim ptr5 As LongPtr
    Dim ptr6 As LongPtr
    Dim ptr7 As LongPtr
    Dim ptr8 As LongPtr
    Dim ptr9 As LongPtr
    '
    Dim v As Variant
    '
    On Error Resume Next
    For Each v In c
    Next v
    Debug.Assert ptr0 = 0
End Sub

Ошибка просто не возникает. Чем это отличается от бега Main2с нашим зацепило? В обоих случаях DISP_E_MEMBERNOTFOUND возвращается, а метод не вызывается.

Что ж, если мы посмотрим на ранее показанные стеки вызовов бок о бок: мы увидим, что не-VB не помещается в стек VB как отдельная запись «Неосновного кода».

По-видимому, ошибка возникает только в том случае, если вызывается метод VBA(либо NewEnum через исходный не-VB Invoke, либо наш собственный IDispatch_Invoke). Если вызывается не-VB метод (например, исходный IDispatch :: Invoke без следующего NewEnum), ошибка не возникает, как в Main3выше. Никаких ошибок не возникает и при работе с коллекцией VBA в тех же обстоятельствах.

Причина ошибки

Как показывают все приведенные выше примеры, ошибку можно резюмировать следующим образом:
вызовы IDispatch::Invokeкоторый, в свою очередь, вызывает, пока указатель стека не был увеличен с размером кадра стека. Следовательно, обе фреймы используют одну и ту же память (вызывающий ShowBug и вызываемый NewEnum).

Обходные пути

Способы принудительного правильного увеличения указателя стека:

  1. вызвать другой метод напрямую (перед строкой), например Sin 1
  2. вызвать другой метод косвенно (перед строкой):
    • звонок в IUnknown::AddRef передав аргумент
    • звонок в IUnknown::QueryInterface используя stdole.IUnknown интерфейс
    • используя оператор, который будет вызывать либо AddRef или Release или оба (например, Set c = c). Мог также позвонить QueryInterface в зависимости от исходного и целевого интерфейсов

Как предлагается в разделе EDIT вопроса, у нас не всегда есть возможность передать класс Custom Collection ByVal потому что это может быть просто глобальная переменная или член класса, и нам нужно не забыть выполнить фиктивный оператор или вызвать другой метод перед For Each... выполняется.

Решение

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

класс:

      VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "EnumHelper"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Private m_enum As IEnumVARIANT

Public Property Set EnumVariant(newEnum_ As IEnumVARIANT)
    Set m_enum = newEnum_
End Property
Public Property Get EnumVariant() As IEnumVARIANT
Attribute EnumVariant.VB_UserMemId = -4
    Set EnumVariant = m_enum
End Property

Public Property Get Self() As EnumHelper
    Set Self = Me
End Property

CustomCollection теперь станет что-то вроде:

      Option Explicit

Private m_coll As Collection

Private Sub Class_Initialize()
    Set m_coll = New Collection
End Sub
Private Sub Class_Terminate()
    Set m_coll = Nothing
End Sub

Public Sub Add(v As Variant)
    m_coll.Add v
End Sub

Public Function NewEnum() As EnumHelper
    With New EnumHelper
        Set .EnumVariant = m_coll.[_NewEnum]
        Set NewEnum = .Self
    End With
End Function

Вам просто нужно позвонить

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

  1. Вам никогда не понадобится добавлять Attribute [MethodName].VB_UserMemId = -4 к любому другому пользовательскому классу коллекции. Это даже более полезно для пользователей, у которых не установлен RubberDuck ( '@Enumerator аннотации), так как им потребуется экспортировать, отредактируйте текстовый файл .cls и импортируйте обратно для каждого настраиваемого класса коллекции
  2. Вы можете предоставить несколько EnumHelpers для одного и того же класса. Рассмотрим настраиваемый класс словаря. Вы могли бы иметь ItemsEnum и KeysEnumв то же время. Оба For Each v in c.ItemsEnum а также For Each v in c.KeysEnum должно сработать
  3. Вы никогда не забудете использовать один из обходных путей, представленных выше, поскольку метод, раскрывающий класс, будет вызываться раньше. Invoke звонит члену ID -4
  4. У вас больше не будет сбоев. Если вы забудете позвонить For Each v in c.NewEnum и вместо этого используйте For Each v in cвы просто получите ошибку времени выполнения, которая все равно будет обнаружена при тестировании. Конечно, вы все равно можете вызвать сбой, передав результат c.NewEnum к другому методу ByRef который затем должен будет выполнить For Each перед вызовом любого другого метода или Setутверждение. Маловероятно, что ты когда-нибудь это сделаешь
  5. Очевидно, но стоит упомянуть, что вы использовали бы тот же EnumHelper class для всех настраиваемых классов коллекций, которые могут быть в проекте

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

Я попытался описать это здесь:

https://docs.microsoft.com/en-us/answers/questions/464383/is-the-vba-64-bit-compiler-broken.html?childToView=545565#answer-545565

Я надеюсь, что тестирование решит эту проблему и для меня, и если да, то примите сердечную благодарность за исследование проблемы и предоставление обходных путей для решения того, что в противном случае означало, что код не может быть перенесен на 64-битный VBA.

Другие вопросы по тегам