x64 カスタムクラスの For Each 列挙のバグ 質問する

x64 カスタムクラスの For Each 列挙のバグ 質問する

数か月前に VBA のバグを発見しましたが、適切な回避策を見つけることができませんでした。このバグは、優れた言語機能を制限してしまうため、非常に厄介です。

カスタム コレクション クラスを使用する場合、クラスをループで使用できるように列挙子が必要になることがよくありますFor Each。これは、次の行を追加することで実現できます。

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

関数/プロパティの署名行の直後に、次のいずれかの方法で記述します。

  1. クラスモジュールをエクスポートし、テキストエディタで内容を編集してからインポートし直す
  2. 使用ゴム製のアヒル'@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地元の人々ローカル変数の値が突然変更されたことを示すウィンドウ:
ここに画像の説明を入力してください
ここで、ptr1 は と等しくなりますObjPtr(c)。メソッド内で使用される変数NewEnum(オプション パラメーターを含む) が多いほど、ShowBugメソッド内の ptr に値 (メモリ アドレス) が書き込まれる回数が多くなります。

言うまでもなく、ローカルポインタメソッド内の変数は、ShowBugアプリケーションのクラッシュを引き起こす可能性が非常に高くなります。

コードを 1 行ずつステップ実行すると、このバグは発生しません。


バグの詳細

Collectionこのバグは、 内部に実際に格納されているものとは関係ありませんCustomCollection。メモリは、NewEnum 関数が呼び出された直後に書き込まれます。したがって、基本的に次のいずれかを実行しても役に立ちません (テスト済み)。

  1. Optionalパラメータの追加
  2. 関数内のコードをすべて削除します(これを示すコードを以下に示します)
  3. IUnknownの代わりに宣言するIEnumVariant
  4. Functionと宣言する代わりにProperty Get
  5. メソッドシグネチャでFriendや のようなキーワードを使用するStatic
  6. DISPID_NEWENUMをさせてまたはセットの対応する得るまたは、前者を非表示にする(つまり、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
    Orで設定し、cByValメソッドへのパラメータShowBug(SetとしてIUnknown::AddRefを呼び出す)
    短所:忘れやすい

  3. EnumHelper列挙にのみ使用される別のクラスを使用します。

    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追加することを忘れやすいです(ランタイム エラーが発生するだけです)。.NewEnumFor Each

最後のアプローチ (3) は、c.NewEnumが実行されると、メソッドが終了し、クラス内のShowBugの呼び出し前に戻るため機能します。基本的に、アプローチ (1) はバグを回避するものです。Property Get EnumVariantEnumHelper


この動作の説明は何ですか? このバグはもっとエレガントな方法で回避できますか?

編集

ByVal を渡すことはCustomCollection常に選択肢になるわけではありません。次の例を考えてみましょう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

ベストアンサー1

何が起こっている

どうやら、スタックフレーム重複しすぎている可能性があります。ShowBugメソッド内に十分な変数があるとクラッシュが防止され、変数の値 (呼び出し元サブルーチン内) は、参照するメモリが、後で呼び出しスタックの先頭に追加/プッシュされた別のスタック フレーム (呼び出されるサブルーチン) でも使用されているため、単純に変更されます。

Debug.Print質問の同じコードにいくつかのステートメントを追加することで、これをテストできます。

クラス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
    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イミディエイト ウィンドウに次のような結果が表示されます。
ここに画像の説明を入力してください

戻り値のアドレスは、明らかにメソッドの変数と変数NewEnumの間のメモリ アドレスにあります。そのため、変数はどこからともなく値を取得します。これは、変数が実際にはメソッドのスタック フレーム(オブジェクトの vtable のアドレスやインターフェイスのアドレスなど) から取得されるためです。変数がそこにない場合、メモリのより重要な部分が上書きされるため (例: メソッドのフレーム ポインター アドレス)、クラッシュは明らかです。メソッドのスタック フレームが大きいほど (たとえば、サイズを増やすためにローカル変数を追加できます)、コール スタックの最上位のスタック フレームと下位のスタック フレームの間で共有されるメモリが多くなります。ptr0ptr9ShowBugNewEnumIEnumVariantShowBugNewEnum

質問に記載されているオプションを使用してバグを回避するとどうなりますか?行Set v = Nothingの前にを追加するだけでFor Each v In c、結果は次のようになります。
ここに画像の説明を入力してください

以前の値と現在の値 (青枠) の両方を表示すると、戻り値がメソッドのおよび変数のNewEnum外側のメモリ アドレスにあることがわかります。回避策を使用してスタック フレームが正しく割り当てられたようです。ptr0ptr9ShowBug

内部でブレークすると、NewEnum呼び出しスタックは次のようになります。
ここに画像の説明を入力してください

For Each呼び出す方法NewEnum

すべてのVBAクラスは以下から派生していますIDディスパッチ(これは IUnknown から派生します)。

For Each...オブジェクトでループが呼び出されると、そのオブジェクトのIDispatch::Invokeメソッドが -4 に等しい値で呼び出されますdispIDMember。VBA.Collection にはすでにそのようなメンバーがありますが、VBA カスタム クラスの場合は、Attribute NewEnum.VB_UserMemId = -4Invoke がメソッドを呼び出せるように独自のメソッドを でマークします。

InvokeFor Each行で使用されているインターフェースが から派生していない場合、 は直接呼び出されませんIDispatch。代わりに、IUnknown::QueryInterfaceが最初に呼び出され、 IDispatch インターフェースが要求されます。 この場合、 は、InvokeIDispatch インターフェースが返された後にのみ呼び出されるのは明らかです。 これが、For Each宣言されたオブジェクトで を使用してもAs IUnknown、渡されるか、グローバルまたはクラス メンバーのカスタム コレクションであるかに関係なく、バグが発生しない理由ですByRef。 質問に記載されている回避策 1 を使用するだけです (つまり、別のメソッドを呼び出します)。これは見えませんが。

フック呼び出し

さらに調査するために、 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://learn.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

IDispatch_Invokeスタック フレームが大きくなるにつれて (したがって、メモリのオーバーラップが大きくなる)、クラッシュを防ぐためにより多くのダミー ptr 変数が必要になることに注意してください。

上記を実行すると、次のようになります。
ここに画像の説明を入力してください

NewEnumメソッドのフックによりコードがメソッドに到達しないにもかかわらず、同じバグが発生しますInvoke。スタック フレームが再び不適切に割り当てられます。

再度、結果のSet v = NothingFor Each v In cに を追加します。ここに画像の説明を入力してください

スタック フレームは正しく割り当てられています (緑の枠線)。これは、問題がメソッドになくNewEnum、置換メソッドにもないことを表しています。 が呼び出されるInvoke前に何かが起きています。Invoke

内部でブレークすると、IDispatch_Invoke呼び出しスタックは次のようになります。
ここに画像の説明を入力してください

最後の例です。空の(コードのない)クラスを考えてみましょう。次のコードを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

バグは発生しません。これは、Main2独自にフックして実行した場合とどう違うのでしょうかInvoke? どちらの場合もDISP_E_MEMBERNOTFOUNDが返され、NewEnumメソッドは呼び出されません。

さて、先ほど示したコールスタックを並べて見てみましょう。
ここに画像の説明を入力してください
非 VB は、Invoke別個の「非基本コード」エントリとして VB スタックにプッシュされていないことがわかります。

どうやら、このバグは、VBA メソッドが呼び出された場合にのみ発生します (元の非 VB Invoke 経由の NewEnum または独自の IDispatch_Invoke)。非 VB メソッドが呼び出された場合 (元の IDispatch::Invoke に続いて NewEnum がない場合など)、Main3上記のようにバグは発生しません。同じ状況で VBA コレクションを実行した場合も、バグは発生しませんFor Each...

バグの原因

上記のすべての例が示唆するように、バグは次のように要約できます:スタック ポインタがスタック フレームのサイズで増分されていない間に、
For Each呼び出しIDispatch::Invokeが呼び出されます。したがって、両方のフレーム (呼び出し元と呼び出し先) で同じメモリが使用されます。NewEnumShowBugShowBugNewEnum

回避策

スタック ポインターの正しい増分を強制する方法:

  1. 別のメソッドを直接(For Each行の前に)呼び出す。例:Sin 1
  2. 別のメソッドを間接的に呼び出します (For Each行の前に):
    • IUnknown::AddRef引数を渡して呼び出すByVal
    • インターフェースIUnknown::QueryInterfaceを使用しての呼び出しstdole.IUnknown
    • またはまたは両方Setを呼び出すステートメントを使用します(例: )。ソースとターゲットのインターフェースに応じて呼び出すこともできます。AddRefReleaseSet c = cQueryInterface

で示唆されているように編集質問のセクションでは、カスタム コレクション クラスを渡す可能性が常にあるとは限りません。これは、カスタム コレクション クラスが単なるグローバル変数またはクラス メンバーである可能性があるためです。その場合、実行前にダミー ステートメントを実行するか、別のメソッドを呼び出すことをByVal覚えておく必要があります。SetFor Each...

解決

質問で提示されたものよりも良い解決策がまだ見つからなかったため、少し調整を加えて、回答の一部としてここにコードを複製することにしました。

EnumHelperクラス:

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カスタム コレクション クラスを実装するすべてのプロジェクトで必要な追加クラスですが、次のような利点もいくつかあります。

  1. Attribute [MethodName].VB_UserMemId = -4 他のカスタムコレクションクラスに を追加する必要はありません。これは、ゴム製のアヒルインストール済み('@Enumerator注釈)では、.clsテキストファイルをエクスポートして編集し、各カスタムコレクションクラスごとにインポートし直す必要があるため
  2. 同じクラスに複数の EnumHelpers を公開することができます。カスタム辞書クラスを考えてみましょう。 とItemsEnumKeysEnum同時に持つことができます。For Each v in c.ItemsEnumと はどちらもFor Each v in c.KeysEnum機能します。
  3. クラスを公開するメソッドはメンバーID -4を呼び出すEnumHelper前に呼び出されるため、上記で示した回避策のいずれかを使用することを忘れることはありません。Invoke
  4. クラッシュはもう発生しません。 with の呼び出しを忘れてFor Each v in c.NewEnum、代わりに use を使用するFor Each v in cと、実行時エラーが発生し、いずれにせよテストで検出されます。 もちろん、 の結果をc.NewEnum別のメソッドに渡すことでクラッシュを強制することはできますが、その場合は他のメソッド呼び出しやステートメントの前に をByRef実行する必要があります。 そんなことはほとんど起こりません。For EachSet
  5. EnumHelper明白ですが言及する価値があります。プロジェクトにあるすべてのカスタムコレクションクラスに同じクラスを使用します。

おすすめ記事