x64 自定义上的 For Each 枚举错误 类

Bug with For Each enumeration on x64 Custom Classes

几个月前我在 VBA 中发现了一个错误,但无法找到合适的解决方法。这个错误真的很烦人,因为它限制了一个很好的语言功能。

使用自定义集合 Class 时,通常需要一个枚举器,以便 class 可以在 For Each 循环中使用。这可以通过添加以下行来完成:

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

紧接在 function/property 签名行之后:

  1. 正在导出 class 模块,在文本编辑器中编辑内容,然后导入回来
  2. 在函数签名上方使用Rubberduck注解'@Enumerator然后同步

不幸的是,在 x64 上,使用 above-mentioned 功能会导致写入错误的内存,并在某些情况下导致应用程序崩溃(稍后讨论)。

重现错误

CustomCollection class:

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方法,代码会在ShowBug方法中停在Assert行,可以在中看到Locals window 局部变量的值突然改变了:

其中 ptr1 等于 ObjPtr(c)NewEnum 方法中使用的变量越多(包括可选参数),ShowBug 方法中写入值(内存地址)的指针越多。

不用说,删除 ShowBug 方法中的局部 ptr 变量肯定会导致应用程序崩溃。

逐行单步执行代码时,不会出现此错误!


有关错误的更多信息

该错误与存储在 CustomCollection 中的实际 Collection 无关。调用 NewEnum 函数后立即写入内存。因此,基本上执行以下任何操作都无济于事(已测试):

  1. 添加Optional参数
  2. 从函数中删除所有代码(见下面的代码)
  3. 声明为 IUnknown 而不是 IEnumVariant
  4. 而不是 Function 声明为 Property Get
  5. 在方法签名中使用 FriendStatic 等关键字
  6. 将 DISPID_NEWENUM 添加到 LetSet 对应 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 行之前发生(before 意味着它可以在同一方法中的任何地方,不一定是之前的确切行):

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

    缺点:容易忘记

  2. 做一个Set语句。它可以在循环中使用的变体上(如果没有使用其他 objects)。正如上面的第 1 点,这需要在执行 For Each 行之前发生:

    Set v = Nothing
    For Each v In c
    Next v
    

    甚至通过 Set c = c
    将集合设置为自身 或者,将 c 参数 ByVal 传递给 ShowBug 方法(作为 Set,调用 IUnknown::AddRef)
    缺点:容易忘记

  3. 使用单独的 EnumHelper class 这是唯一用于枚举的 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 class 中删除。

    优点:在 .NewEnum 函数上强制 For Each 而不是直接自定义集合。这避免了由错误引起的任何崩溃。

    缺点:总是需要额外的 EnumHelper class。很容易忘记在 For Each 行中添加 .NewEnum(只会触发运行时错误)。

最后一种方法 (3) 有效,因为当执行 c.NewEnum 时, ShowBug 方法退出,然后在 EnumHelper 中调用 Property Get EnumVariant 之前返回class。基本上方法 (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

显然,该示例有点勉强,但“parent”object 包含“child”object 的自定义集合是很常见的s 和“parent”可能想做一些涉及部分或全部“children”的操作。

在这种情况下,很容易忘记在 For Each 行之前执行 Set 语句或方法调用。

发生了什么事

看起来 stack frames 是重叠的,尽管它们不应该重叠。在 ShowBug 方法中有足够的变量可以防止崩溃并且变量的值(在调用者子例程中)被简单地更改,因为它们引用的内存也被另一个堆栈帧(被调用的子例程)使用 added/pushed 稍后在调用堆栈的顶部。

我们可以通过向问题中的相同代码添加几个 Debug.Print 语句来测试这一点。

CustomCollection class:

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

通过 运行ning Main 我立即得到了这样的东西 Window:

NewEnum return 值的地址显然位于 ShowBug 方法的 ptr0ptr9 变量之间的内存地址。所以,这就是为什么变量不知从哪里获取值的原因,因为它们实际上来自 NewEnum 方法的堆栈帧(比如 object 的 vtable 的地址或 IEnumVariant 界面)。如果变量不在那里,那么崩溃是显而易见的,因为内存的更多关键部分正在被覆盖(例如 ShowBug 方法的帧指针地址)。由于 NewEnum 方法的堆栈帧较大(例如,我们可以添加局部变量以增加大小),调用堆栈中顶部堆栈帧和下方堆栈帧之间共享的内存越多。

如果我们使用问题中描述的选项解决错误,会发生什么情况?只需在 For Each v In c 行前添加一个 Set v = Nothing,结果为:

同时显示之前的值和当前值(蓝色边框),我们可以看到 NewEnum return 位于 ptr0 和 [=23 之外的内存地址=] ShowBug 方法的变量。似乎使用变通方法正确分配了堆栈帧。

如果我们在 NewEnum 内部中断,调用堆栈如下所示:

For Each如何调用NewEnum

每个 VBA class 都派生自 IDispatch(后者又派生自 IUnknown)。

当在 object 上调用 For Each... 循环时,调用 object 的 IDispatch::Invoke 方法时 dispIDMember 等于 -4 . VBA.Collection 已经有这样的成员,但是对于 VB 自定义的 classes,我们用 Attribute NewEnum.VB_UserMemId = -4 标记我们自己的方法,以便 Invoke 可以调用我们的方法。

如果 For Each 行中使用的接口不是从 IDispatch 派生的,则不会直接调用

Invoke。相反,首先调用 IUnknown::QueryInterface 并询问 IDispatch 接口。在这种情况下 Invoke 显然被称为 只有在 IDispatch 接口被 returned 之后。这就是为什么在声明为 As IUnknown 的 Object 上使用 For Each 不会导致错误的原因,无论它是通过 ByRef 还是全局或 class会员定制collection。它只是使用问题中提到的解决方法 1(即调用另一种方法),尽管我们看不到它。

挂钩调用

我们可以用我们自己的方法替换 non-VB Invoke 方法以便进一步调查。在标准 .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

我们 运行 Main2 方法(标准 .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 变量来防止崩溃,因为 IDispatch_Invoke 的堆栈帧更大(因此,内存重叠更大)。

通过 运行 以上,我得到:

尽管由于 Invoke 方法的挂钩,代码从未到达 NewEnum 方法,但仍会出现相同的错误。堆栈帧再次被错误分配。

同样,在 For Each v In c 之前添加一个 Set v = Nothing 结果为:

堆栈帧分配正确(带绿色边框)。这表明问题不在于 NewEnum 方法,也不在于我们的替换 Invoke 方法。在调用我们的 Invoke 之前发生了一些事情。

如果我们在 IDispatch_Invoke 内部中断,调用堆栈如下所示:

最后一个例子。考虑一个空白(没有代码)class Class1。如果我们在下面的代码中运行Main3

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

这个错误根本就没有发生。这与 运行ning Main2 与我们自己的 hooked Invoke 有何不同?在这两种情况下,DISP_E_MEMBERNOTFOUND 都被 return 编辑并且没有调用 NewEnum 方法。

好吧,如果我们并排查看前面显示的调用堆栈:

我们可以看到 non-VB Invoke 没有作为单独的“Non-Basic 代码”条目推送到 VB 堆栈。

显然,只有在调用 VBA 方法(通过原始 non-VB Invoke 或我们自己的 IDispatch_Invoke 调用 NewEnum)时才会出现该错误。如果调用了 non-VB 方法(就像没有后续 NewEnum 的原始 IDispatch::Invoke 一样),则不会出现上述 Main3 中的错误。 运行ning For Each... on a 时没有错误发生VBA Collection同样情况下也可以。

错误原因

正如以上所有示例所暗示的,该错误可总结如下:
For Each 调用 IDispatch::Invoke,后者又调用 NewEnum,而堆栈指针尚未随着 ShowBug 堆栈帧的大小递增。因此,两个框架(调用者 ShowBug 和被调用者 NewEnum)使用相同的内存。

解决方法

强制堆栈指针正确递增的方法:

  1. 直接调用另一个方法(在 For Each 行之前),例如Sin 1
  2. 间接调用另一个方法(在For Each行之前):
    • 通过传递参数 ByVal
    • 调用 IUnknown::AddRef
    • 使用stdole.IUnknown接口调用IUnknown::QueryInterface
    • 使用 Set 语句,该语句将调用 AddRefRelease 或两者(例如 Set c = c)。也可以根据源和目标接口调用 QueryInterface

正如问题的 EDIT 部分所建议的,我们并不总是有可能通过自定义 Collection class ByVal 因为它可能只是一个全局变量,或者是一个 class 成员,我们需要记住在执行 For Each... 之前做一个伪 Set 语句或调用另一个方法。

解决方案

我仍然找不到比问题中提出的解决方案更好的解决方案,因此我将在此处复制代码作为答案的一部分,并稍作调整。

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

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

你只需要用For Each v in c.NewEnum

打电话

尽管 EnumHelper class 在任何实施自定义 collection class 的项目中都需要额外的 class,但有几个还有优点:

  1. 您永远不需要将 Attribute [MethodName].VB_UserMemId = -4 添加到任何其他自定义 collection class。这对于没有安装 RubberDuck'@Enumerator 注释)的用户更有用,因为他们需要导出、编辑 .cls 文本文件并为每个自定义 collection 导入回class
  2. 您可以为同一个 class 公开多个 EnumHelper。考虑一个自定义字典 ​​class。您可以同时拥有 ItemsEnumKeysEnumFor Each v in c.ItemsEnumFor Each v in c.KeysEnum 都可以
  3. 您永远不会忘记使用上述解决方法之一,因为在 Invoke 调用成员 ID -4[=234 之前将调用公开 EnumHelper class 的方法=]
  4. 你不会再崩溃了。如果您忘记使用 For Each v in c.NewEnum 调用,而是使用 For Each v in c,您只会得到一个 运行 时间错误,无论如何都会在测试中发现。当然,您仍然可以通过将 c.NewEnum 的结果传递给另一个方法 ByRef 来强制崩溃,然后需要在任何其他方法调用或 Set 语句之前执行 For Each .你极不可能那样做
  5. 显而易见但值得一提的是,您将对项目中可能拥有的所有自定义 collection class 使用相同的 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.