数か月前に VBA のバグを発見しましたが、適切な回避策を見つけることができませんでした。このバグは、優れた言語機能を制限してしまうため、非常に厄介です。
カスタム コレクション クラスを使用する場合、クラスをループで使用できるように列挙子が必要になることがよくありますFor Each
。これは、次の行を追加することで実現できます。
Attribute [MethodName].VB_UserMemId = -4 'The reserved DISPID_NEWENUM
関数/プロパティの署名行の直後に、次のいずれかの方法で記述します。
- クラスモジュールをエクスポートし、テキストエディタで内容を編集してからインポートし直す
- 使用ゴム製のアヒル
'@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 関数が呼び出された直後に書き込まれます。したがって、基本的に次のいずれかを実行しても役に立ちません (テスト済み)。
Optional
パラメータの追加- 関数内のコードをすべて削除します(これを示すコードを以下に示します)
IUnknown
の代わりに宣言するIEnumVariant
Function
と宣言する代わりにProperty Get
- メソッドシグネチャで
Friend
や のようなキーワードを使用するStatic
- 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
同じバグが発生します。
回避策
私が見つけた、バグを回避するための信頼できる方法:
メソッドを呼び出し(基本的には
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
Orで設定し、cByVal
メソッドへのパラメータShowBug
(SetとしてIUnknown::AddRefを呼び出す)
短所:忘れやすい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
追加することを忘れやすいです(ランタイム エラーが発生するだけです)。.NewEnum
For Each
最後のアプローチ (3) は、c.NewEnum
が実行されると、メソッドが終了し、クラス内のShowBug
の呼び出し前に戻るため機能します。基本的に、アプローチ (1) はバグを回避するものです。Property Get EnumVariant
EnumHelper
この動作の説明は何ですか? このバグはもっとエレガントな方法で回避できますか?
編集
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 のアドレスやインターフェイスのアドレスなど) から取得されるためです。変数がそこにない場合、メモリのより重要な部分が上書きされるため (例: メソッドのフレーム ポインター アドレス)、クラッシュは明らかです。メソッドのスタック フレームが大きいほど (たとえば、サイズを増やすためにローカル変数を追加できます)、コール スタックの最上位のスタック フレームと下位のスタック フレームの間で共有されるメモリが多くなります。ptr0
ptr9
ShowBug
NewEnum
IEnumVariant
ShowBug
NewEnum
質問に記載されているオプションを使用してバグを回避するとどうなりますか?行Set v = Nothing
の前にを追加するだけでFor Each v In c
、結果は次のようになります。
以前の値と現在の値 (青枠) の両方を表示すると、戻り値がメソッドのおよび変数のNewEnum
外側のメモリ アドレスにあることがわかります。回避策を使用してスタック フレームが正しく割り当てられたようです。ptr0
ptr9
ShowBug
内部でブレークすると、NewEnum
呼び出しスタックは次のようになります。
For Each
呼び出す方法NewEnum
すべてのVBAクラスは以下から派生していますIDディスパッチ(これは IUnknown から派生します)。
For Each...
オブジェクトでループが呼び出されると、そのオブジェクトのIDispatch::Invoke
メソッドが -4 に等しい値で呼び出されますdispIDMember
。VBA.Collection にはすでにそのようなメンバーがありますが、VBA カスタム クラスの場合は、Attribute NewEnum.VB_UserMemId = -4
Invoke がメソッドを呼び出せるように独自のメソッドを でマークします。
Invoke
For Each
行で使用されているインターフェースが から派生していない場合、 は直接呼び出されませんIDispatch
。代わりに、IUnknown::QueryInterface
が最初に呼び出され、 IDispatch インターフェースが要求されます。 この場合、 は、Invoke
IDispatch インターフェースが返された後にのみ呼び出されるのは明らかです。 これが、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 = Nothing
前For 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
が呼び出されます。したがって、両方のフレーム (呼び出し元と呼び出し先) で同じメモリが使用されます。NewEnum
ShowBug
ShowBug
NewEnum
回避策
スタック ポインターの正しい増分を強制する方法:
- 別のメソッドを直接(
For Each
行の前に)呼び出す。例:Sin 1
- 別のメソッドを間接的に呼び出します (
For Each
行の前に):IUnknown::AddRef
引数を渡して呼び出すByVal
- インターフェース
IUnknown::QueryInterface
を使用しての呼び出しstdole.IUnknown
- またはまたは両方
Set
を呼び出すステートメントを使用します(例: )。ソースとターゲットのインターフェースに応じて呼び出すこともできます。AddRef
Release
Set c = c
QueryInterface
で示唆されているように編集質問のセクションでは、カスタム コレクション クラスを渡す可能性が常にあるとは限りません。これは、カスタム コレクション クラスが単なるグローバル変数またはクラス メンバーである可能性があるためです。その場合、実行前にダミー ステートメントを実行するか、別のメソッドを呼び出すことをByVal
覚えておく必要があります。Set
For 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
カスタム コレクション クラスを実装するすべてのプロジェクトで必要な追加クラスですが、次のような利点もいくつかあります。
Attribute [MethodName].VB_UserMemId = -4
他のカスタムコレクションクラスに を追加する必要はありません。これは、ゴム製のアヒルインストール済み('@Enumerator
注釈)では、.clsテキストファイルをエクスポートして編集し、各カスタムコレクションクラスごとにインポートし直す必要があるため- 同じクラスに複数の EnumHelpers を公開することができます。カスタム辞書クラスを考えてみましょう。 と
ItemsEnum
をKeysEnum
同時に持つことができます。For Each v in c.ItemsEnum
と はどちらもFor Each v in c.KeysEnum
機能します。 - クラスを公開するメソッドはメンバーID -4を呼び出す
EnumHelper
前に呼び出されるため、上記で示した回避策のいずれかを使用することを忘れることはありません。Invoke
- クラッシュはもう発生しません。 with の呼び出しを忘れて
For Each v in c.NewEnum
、代わりに use を使用するFor Each v in c
と、実行時エラーが発生し、いずれにせよテストで検出されます。 もちろん、 の結果をc.NewEnum
別のメソッドに渡すことでクラッシュを強制することはできますが、その場合は他のメソッド呼び出しやステートメントの前に をByRef
実行する必要があります。 そんなことはほとんど起こりません。For Each
Set
EnumHelper
明白ですが言及する価値があります。プロジェクトにあるすべてのカスタムコレクションクラスに同じクラスを使用します。