Ошибка с перечислением для каждого в пользовательских классах x64
Несколько месяцев назад я обнаружил ошибку в VBA и не смог найти достойного решения. Ошибка действительно раздражает, так как она как бы ограничивает приятную языковую функцию.
При использовании Custom Collection Class довольно часто требуется иметь перечислитель, чтобы этот класс можно было использовать в
For Each
петля. Это можно сделать, добавив эту строку:
Attribute [MethodName].VB_UserMemId = -4 'The reserved DISPID_NEWENUM
сразу после строки подписи функции / свойства либо:
- Экспорт модуля класса, редактирование содержимого в текстовом редакторе и последующий импорт обратно
- Использование аннотации
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. Итак, выполнение любого из следующих действий не помогает (проверено):
- добавление
Optional
параметры - удаление всего кода из функции (см. ниже код, показывающий это)
- объявив как
IUnknown
вместоIEnumVariant
- вместо
Function
объявив какProperty Get
- используя такие ключевые слова, как
Friend
или жеStatic
в сигнатуре метода - добавление 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
выдает ту же ошибку.
Обходной путь
Надежные способы, которые я нашел, чтобы избежать ошибки:
Вызвать метод (в основном оставить
ShowBug
метод) и вернитесь. Это должно произойти доFor Each
строка выполняется (до значения, что она может быть где угодно в том же методе, не обязательно в точной строке до):Sin 0 'Or VBA.Int 1 - you get the idea For Each v In c Next v
Минусы: легко забыть
Сделать
Set
заявление. Это может быть вариант, используемый в цикле (если не используются другие объекты). Как и в пункте 1 выше, это должно произойти доFor Each
строка выполняется:Set v = Nothing For Each v In c Next v
или даже установив коллекцию для себя с помощью
Set c = c
Или, передав параметр cByVal
кShowBug
метод (который, как и Set, вызывает IUnknonw::AddRef)
Минусы: легко забытьИспользование отдельного
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) позволяет избежать ошибки.
Чем объясняется такое поведение? Можно ли избежать этой ошибки более элегантным способом?
РЕДАКТИРОВАТЬ
Прохождение
CustomCollection
ByVal - не всегда вариант. Рассмотрим
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
).
Обходные пути
Способы принудительного правильного увеличения указателя стека:
- вызвать другой метод напрямую (перед строкой), например
Sin 1
- вызвать другой метод косвенно (перед строкой):
- звонок в
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
Вам просто нужно позвонить
Хотя этот класс будет дополнительным классом, необходимым в любом проекте, реализующем настраиваемый класс коллекции, у него также есть несколько преимуществ:
- Вам никогда не понадобится добавлять
Attribute [MethodName].VB_UserMemId = -4
к любому другому пользовательскому классу коллекции. Это даже более полезно для пользователей, у которых не установлен RubberDuck ('@Enumerator
аннотации), так как им потребуется экспортировать, отредактируйте текстовый файл .cls и импортируйте обратно для каждого настраиваемого класса коллекции - Вы можете предоставить несколько EnumHelpers для одного и того же класса. Рассмотрим настраиваемый класс словаря. Вы могли бы иметь
ItemsEnum
иKeysEnum
в то же время. ОбаFor Each v in c.ItemsEnum
а такжеFor Each v in c.KeysEnum
должно сработать - Вы никогда не забудете использовать один из обходных путей, представленных выше, поскольку метод, раскрывающий класс, будет вызываться раньше.
Invoke
звонит члену ID -4 - У вас больше не будет сбоев. Если вы забудете позвонить
For Each v in c.NewEnum
и вместо этого используйтеFor Each v in c
вы просто получите ошибку времени выполнения, которая все равно будет обнаружена при тестировании. Конечно, вы все равно можете вызвать сбой, передав результатc.NewEnum
к другому методуByRef
который затем должен будет выполнитьFor Each
перед вызовом любого другого метода илиSet
утверждение. Маловероятно, что ты когда-нибудь это сделаешь - Очевидно, но стоит упомянуть, что вы использовали бы тот же
EnumHelper
class для всех настраиваемых классов коллекций, которые могут быть в проекте
Я не могу добавить комментарий из-за отсутствия достаточного количества репутации, и я не могу использовать раздел чата, поскольку он заморожен, но я хотел добавить, что я столкнулся с чем-то, что звучит устрашающе похожим, и хотя я еще не тестировал любое из представленных здесь решений, похоже, что это одна и та же ошибка.
Я попытался описать это здесь:
Я надеюсь, что тестирование решит эту проблему и для меня, и если да, то примите сердечную благодарность за исследование проблемы и предоставление обходных путей для решения того, что в противном случае означало, что код не может быть перенесен на 64-битный VBA.