[VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応)

2022年3月20日

はじめに

今回はVBA単独でFor Eachに対応した自作クラスを作成する方法を説明します。この手の説明では、よくCollectionかDictionaryの_NewEnum()を呼び出す手法を紹介しているサイトがありますが、本記事では任意のデータ構造をFor Eachに対応させます。具体的には、WindowsAPIを使用してIEnumVARIANTインターフェイスを持つCOMオブジェクトをメモリ上に作成します。なお、今回説明する実装は、ウィルス対策ソフトが反応して実効速度が遅くなる場合があるため、環境次第では注意して下さい(ちなみにAviraは大丈夫でした)。

前提条件

ソースコードはVBA中級者以上 兼 ポインタを使用する言語の経験者でないと理解できません。そのため、この記事の読者は、ある程度の力量があることを前提とし、VBEの基本的な機能や使い方は説明しません。本記事の本文ではマクロ有効ブックにForEachを実装する手順のみを解説しております。動作原理やアルゴリズム等は全てソースコードのコメントに残しているため、必要な場合はそちらを読んで下さい。
(コメント欄等で質問頂ければ基本的にはお答えします)

説明

IEnumVARIANTインターフェイスを持つCOMオブジェクトの生成用関数の定義

適当なマクロ有効ブックを用意します。

標準モジュールを新規追加し、名前をMEnumeratorBaseに変更して、下記のコードを貼り付けます。

' [VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応) 
' Copyright (c) 2020-2024  黒箱 
' This software is released under the GPLv3. 
' このソフトウェアはGNU GPLv3の下でリリースされています。 

'/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
' @file           MEnumeratorBase
' @brief          IEnumVARIANTを生成するための共通関数
' @date           2020.11.15
' $Version:       1
' $Revision:      0
' @note           なし
'
'/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Option Explicit

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
Private Declare PtrSafe Function CoTaskMemAlloc Lib "ole32" (ByVal cb As LongPtr) As LongPtr
Private Declare PtrSafe Function IIDFromString Lib "ole32" (ByVal lpsz As LongPtr, ByVal lpiid As LongPtr) As Long
Private Declare PtrSafe Sub ZeroMemory Lib "kernel32" Alias "RtlZeroMemory" (dest As Any, ByVal numBytes As LongPtr)


'NULLの代用
Private Const NUL As LongPtr = 0


'COMのエラー処理で使用するHRESULT型(一部) https://docs.microsoft.com/en-us/windows/desktop/learnwin32/error-handling-in-com
Public Enum HRESULT
    E_UNEXPECTED = &H8000FFFF   '予期しない状態。
    E_INVALIDARG = &H80070057   'パラメータ値が無効です。
    E_OUTOFMEMORY = &H8007000E  'メモリ不足。
    E_ACCESSDENIED = &H80070005 'アクセスが拒否されました。
    E_FAIL = &H80004005         '不特定のエラー。
    E_POINTER = &H80004003      'ポインタ値に対してNULLが誤って渡されました。
    E_NOINTERFACE = &H80004002  'インターフェイスが存在しません。
    E_NOTIMPL = &H80004001      'インターフェイスが実装されていません。
    S_OK = &H0                  '成功
    S_FALSE = &H1               '成功
End Enum

'IEnumVARIANT::Nextの引数rbVarの型として使用
Public Type EnumVariables
    Element(0) As Variant
End Type


'* @fn Public Function CreateComEnumerator(ByVal EnumMemberPtr As LongPtr, ByVal EnumMemberSize As LongPtr) As IEnumVARIANT
'* @brief IEnumVARIANTインターフェイスを持つCOMオブジェクトのインスタンスを生成します。
'* @param[in] EnumMemberPtr IEnumVARIANTインターフェイスを持つCOMオブジェクトのメンバ変数(構造体)へのポインタを指定します。
'* @param[in] EnumMemberSize EnumMemberPtrの実際のバイト数を指定します。
'* @return 生成したインスタンスを返します。
'* @details この関数の実際の機能は、引数EnumMemberPtrで指定されたメンバを使用してCOMオブジェクトを生成し、これをIEnumVARIANT型にキャストするだけです。生成されたオブジェクトがIEnumVARIANTとして振舞うためには、EnumMemberPtrに適切な仮想関数テーブルを設定しておく必要があります。
'*
Public Function CreateComEnumerator(ByVal EnumMemberPtr As LongPtr, ByVal EnumMemberSize As LongPtr) As IEnumVARIANT
    
    'COMオブジェクト用のヒープメモリ確保
    Dim pThis As LongPtr: pThis = CoTaskMemAlloc(EnumMemberSize)
    If (pThis = NUL) Then Exit Function
    
    'EnumMemberPtrの内容からIEnumVARIANTのインスタンスを作成
    Call CopyMemory(ByVal pThis, ByVal EnumMemberPtr, EnumMemberSize)
    
    '呼び出し元関数のスタック解放時に、EnumMember内の配列またはオブジェクトの参照カウントがデクリメントされないようにする
    Call ZeroMemory(ByVal EnumMemberPtr, EnumMemberSize)
    
    '戻り値に作成したオブジェクトを設定
    UnsafeSet(VarPtr(CreateComEnumerator)) = pThis

End Function

'* @fn Private Function CopyIidToStringType(ByVal riid As LongPtr) As String
'* @brief IIDを文字列型に格納します。
'* @param[in] RIid IIDへの参照を指定します。
'* @return
'* @details IIDは16バイトの構造体であるため、比較演算を容易にするために文字列型に格納します。
'* @note Type IID
'* @note     Data1 As Long
'* @note     Data2 As Integer
'* @note     Data3 As Integer
'* @note     Data4(0 To 7) As Byte
'* @note End Type
'*
Private Function CopyIidToStringType(ByVal RIid As LongPtr) As String
    'IID(GUID)は16バイト、VBAの1文字は2バイトであるためバッファは8文字
    CopyIidToStringType = Space(8)
    Call CopyMemory(ByVal StrPtr(CopyIidToStringType), ByVal RIid, 16)
End Function


'* @fn Private Function GetIidFromText(ByRef IidText As String) As String
'* @brief IIDを示す文字列からIID(文字列型)を生成します。
'* @param[in] IidText IIDを示す文字列を指定します。文字列は{}で囲まれた16進数とハイフンの組合せです。
'* @return IIDを文字列型で返します。
'* @details IIDは本来16バイトの構造体ですが、比較演算を容易にするために文字列型に格納して返します。
'*
Private Function GetIidFromText(ByRef IidText As String) As String
    GetIidFromText = Space(8)
    Call IIDFromString(StrPtr(IidText), StrPtr(GetIidFromText))
End Function


'引数を戻り値に返すだけの関数です。AddressOfで取得した関数ポインタを変数に格納するために使用します。
Public Function GetAddressOf(ByVal FunctionPointer As LongPtr) As LongPtr
    GetAddressOf = FunctionPointer
End Function

'* @fn Public Function RiidIsIEnumerator(ByVal riid As LongPtr) As Boolean
'* @brief 引数で指定したRIIDをチェックし、IUnknownかIEnumVARIANTの場合はTrue、それ以外はFalseを返します。
'* @param[in] riid  チェックするIIDへのポインタを指定します。
'* @return 引数で指定したRIIDがIUnknownかIEnumVARIANTの場合はTrue、それ以外はFalseを返します。
'* @details
'*
Public Function RIidIsIEnumerator(ByVal RIid As LongPtr) As Boolean

    Static IidOfIUnknown As String
    Static IidOfIEnumVARIANT As String
    
    Static Init As Boolean
    If Not (Init) Then
        IidOfIUnknown = GetIidFromText("{00000000-0000-0000-C000-000000000046}")
        IidOfIEnumVARIANT = GetIidFromText("{00020404-0000-0000-C000-000000000046}")
        Init = True
    End If
    
    
    Select Case CopyIidToStringType(RIid)
        Case IidOfIUnknown:     RIidIsIEnumerator = True
        Case IidOfIEnumVARIANT: RIidIsIEnumerator = True
        Case Else:              RIidIsIEnumerator = False
    End Select


End Function


'* @fn Public Property Let UnsafeSet(ByVal ppLHS As LongPtr, ByVal pRHS As LongPtr)
'* @brief LongPtr型変数の値(アドレス)を参照型に代入します。
'* @param[out] RefTypeVarAddress 代入される参照型のアドレスをしてします。この引数には通常VarPtr(obj)を指定します。
'* @param[in]  Address  代入するアドレスを指定します。
'* @details  このプロパティの実際の機能は、ポインタ変数へのアドレス(RefTypeVarAddress)を逆参照して、これに指定したアドレス(Address)を代入します。
'*
Public Property Let UnsafeSet(ByVal RefTypeVarAddress As LongPtr, ByVal Address As LongPtr)
#If Win64 Then
    Const SizeOfPointer = 8
#Else
    Const SizeOfPointer = 4
#End If
    Call CopyMemory(ByVal RefTypeVarAddress, Address, SizeOfPointer)
End Property

標準モジュールを新規追加し、名前をMEnumeratorに変更して、下記のコードを貼り付けます。

' [VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応) 
' Copyright (c) 2020-2024  黒箱 
' This software is released under the GPLv3. 
' このソフトウェアはGNU GPLv3の下でリリースされています。 

'/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
' @file           MEnumerator
' @brief          IEnumeratorを呼び出すIEnumVARIANTを生成するための関数
' @date           2020.11.15
' $Version:       1
' $Revision:      0
' @note           カスタムクラス設計者はIIteratorインターフェイスを実装したクラスのインスタンをNew_Enumerator()に渡すことで列挙子を生成できます。
'
'/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Option Explicit

Private Declare PtrSafe Sub CoTaskMemFree Lib "ole32" (ByVal pv As LongPtr)

#If Win64 Then
'    '下記APIが見付からないためコメントアウト
'    Public Declare PtrSafe Function InterlockedIncrement64 Lib "kernel32" (ByRef lpAddend As LongLong) As LongLong
'    Public Declare PtrSafe Function InterlockedDecrement64 Lib "kernel32" (ByRef lpAddend As LongLong) As LongLong
#Else
    Private Declare PtrSafe Function InterlockedIncrement Lib "kernel32" (ByRef Addend As Long) As Long
    Private Declare PtrSafe Function InterlockedDecrement Lib "kernel32" (ByRef Addend As Long) As Long
#End If

Private Const NUL As LongPtr = 0

'EnumVARIANTのメンバ
Private Type SEnumVARIANT
    VTablePtr  As LongPtr
    References As LongPtr
    Iterator   As IIterator
End Type


'VTable
Private VTable(6) As LongPtr

'* @fn Private Sub InitVTable()
'* @brief IEnumVariant用の仮想関数テーブルを初期化します。
'*
Private Sub InitVTable()
    VTable(0) = GetAddressOf(AddressOf IUnknown_QueryInterface)
    VTable(1) = GetAddressOf(AddressOf IUnknown_AddRef)
    VTable(2) = GetAddressOf(AddressOf IUnknown_Release)
    VTable(3) = GetAddressOf(AddressOf IEnumVARIANT_Next)
    VTable(4) = GetAddressOf(AddressOf IEnumVARIANT_Skip)
    VTable(5) = GetAddressOf(AddressOf IEnumVARIANT_Reset)
    VTable(6) = GetAddressOf(AddressOf IEnumVARIANT_Clone)
End Sub


'* @fn Public Function New_Iterator(ByVal Iterator As IIterator) As IEnumVARIANT
'* @brief ForEach内でIIteratorを実行するCOMオブジェクトを生成します。
'* @param[in] Iterator ForEachで呼び出されるIIteratorのインスタンスを指定します。
'* @return IEnumVARIANTインターフェイスを持つCOMオブジェクトを返します。
'*
Public Function New_Enumerator(ByVal Iterator As IIterator) As IEnumVARIANT
    If (VTable(0) = NUL) Then Call InitVTable
    
    Dim this As SEnumVARIANT
    With this
        .VTablePtr = VarPtr(VTable(0))
        .References = 1
        Set .Iterator = Iterator
    End With
    
    Set New_Enumerator = CreateComEnumerator(VarPtr(this), LenB(this))


End Function


'* @fn Private Function IUnknown_QueryInterface(ByRef this As SEnumVARIANT, ByVal RIid As LongPtr, ByVal ppvObject As LongPtr) As Long
'* @brief COMオブジェクト対してインターフェイスの照会を行います。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @param[in] RIid 照会されているインターフェースのIIDへの参照。
'* @param[in] ppvObject riidパラメーターで指定されたIIDを持つインターフェースへのポインターのアドレス。
'* @return ppvObjectがNULLならE_POINTER、照会されているインタフェースを実装している場合はS_OK、それ以外の場合はE_NOINTERFACEを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void)
'*
Private Function IUnknown_QueryInterface(ByRef this As SEnumVARIANT, ByVal RIid As LongPtr, ByVal ppvObject As LongPtr) As Long
    If ppvObject = NUL Then
        IUnknown_QueryInterface = E_POINTER
        Exit Function
    End If

    If Not (RIidIsIEnumerator(RIid)) Then
        IUnknown_QueryInterface = E_NOINTERFACE
        Exit Function
    End If
    
    UnsafeSet(ppvObject) = VarPtr(this)
    Call IUnknown_AddRef(this)
    IUnknown_QueryInterface = S_OK
    
End Function


'* @fn Private Function IUnknown_AddRef(ByRef this As IteratorMemberr) As LongLong
'* @brief COMオブジェクトへのインターフェイスポインタの参照カウントをインクリメントします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @return 新しい参照カウントを返します。この値は、テスト目的でのみ使用することを目的としています。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-addref
'*
#If Win64 Then
    Private Function IUnknown_AddRef(ByRef this As SEnumVARIANT) As LongLong
        
        'InterlockedIncrement64がkernel32.dllで見付からないため、スレッドアンセーフ状態で参照カウントをインクリメント
        'IUnknown_AddRef = InterlockedIncrement64(this.References)
        this.References = this.References + 1
        IUnknown_AddRef = this.References
    End Function

#Else
    Private Function IUnknown_AddRef(ByRef this As SEnumVARIANT) As Long
        'スレッドセーフ状態で参照カウントをインクリメント
        IUnknown_AddRef = InterlockedIncrement(this.References)
    End Function
#End If


'* @fn Private Function IUnknown_Release(ByRef this As IteratorMemberr) As LongLong
'* @brief COMオブジェクトのインターフェイスの参照カウントを減らします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @return 新しい参照カウントを返します。この値は、テスト目的でのみ使用することを目的としています。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-Release
'*
#If Win64 Then
    Private Function IUnknown_Release(ByRef this As SEnumVARIANT) As LongLong
    
        'InterlockedDecrement64がkernel32.dllで見付からないため、スレッドアンセーフ状態で参照カウントをデクリメント
        'IUnknown_Release = InterlockedDecrement64(this.References)
        this.References = this.References - 1
        IUnknown_Release = this.References
        
        '参照カウントが0になったらインスタンスのメモリを解放
        If (IUnknown_Release = 0) Then
            Set this.Iterator = Nothing
            Call CoTaskMemFree(VarPtr(this))
        End If
        
    End Function

#Else
    Private Function IUnknown_Release(ByRef this As SEnumVARIANT) As Long
        
        'スレッドセーフ状態で参照カウントをデクリメント
        IUnknown_Release = InterlockedDecrement(this.References)
        
        '参照カウントが0になったらインスタンスのメモリを解放
        If (IUnknown_Release = 0) Then
            Set this.Iterator = Nothing
            Call CoTaskMemFree(VarPtr(this))
        End If
        
    End Function

#End If




'* @fn Private Function IEnumVARIANT_Next(ByRef this As IteratorMemberr, ByVal celt As Long, ByRef rgVar As EnumVariables, ByRef pCeltFetched As Long) As Long
'* @brief 列挙シーケンスで指定された要素を取得します。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @param[in] celt 取得する要素の数
'* @param[out] rgVar 取得した要素を返す配列(をメンバに持つ構造体)。
'* @param[out] pCeltFetched NULL参照でない場合、rgVarで返した要素の数を返します。
'* @return 返される要素の数がceltと同じ場合はS_OK、返される要素の数がcelt未満の場合はS_FALSEを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-ienumvariant-next
'*
Private Function IEnumVARIANT_Next(ByRef this As SEnumVARIANT, ByVal celt As Long, ByRef rgVar As EnumVariables, ByRef pCeltFetched As Long) As Long
    
    If (VarPtr(rgVar.Element(0)) = NUL) Then
        IEnumVARIANT_Next = E_POINTER
        Exit Function
    End If

    Dim Fetched As Long
    Do While (this.Iterator.GetNext(rgVar.Element(Fetched)))
        
        Fetched = Fetched + 1
        If (Fetched = celt) Then
            If VarPtr(pCeltFetched) Then pCeltFetched = Fetched
           IEnumVARIANT_Next = S_OK
            Exit Function
        End If
    Loop

    If VarPtr(pCeltFetched) Then pCeltFetched = Fetched
    IEnumVARIANT_Next = S_FALSE

End Function


'* @fn Private Function IEnumVARIANT_Skip(ByRef this As SEnumVARIANT, ByVal celt As Long) As Long
'* @brief 列挙シーケンス内の要素をスキップしようとします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @param[in] celt スキップする要素の数
'* @return 指定された要素数をスキップした場合はS_OK、要求された数の要素をスキップする前に最後の要素に到達した場合はS_FALSEを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-ienumvariant-skip
'*
Private Function IEnumVARIANT_Skip(ByRef this As SEnumVARIANT, ByVal celt As Long) As Long
    
    With this.Iterator
        Dim e As Variant
        Dim i As Long
        For i = 1 To celt
            If Not (.GetNext(e)) Then
                IEnumVARIANT_Skip = S_FALSE
                Exit Function
            End If
        Next
    End With
    
    IEnumVARIANT_Skip = S_OK
End Function


'* @fn Private Function IEnumVARIANT_Reset(ByRef this As SEnumVARIANT) As Long
'* @brief 列挙シーケンスを最初にリセットします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @return 関数が成功した場合はS_OK、失敗した場合はS_FALSEを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-ienumvariant-reset
'*
Private Function IEnumVARIANT_Reset(ByRef this As SEnumVARIANT) As Long
    If (this.Iterator.Reset) Then
        IEnumVARIANT_Reset = S_OK
    Else
        IEnumVARIANT_Reset = S_FALSE
    End If
End Function



'* @fn Private Function IEnumVARIANT_Reset(ByRef this As SEnumVARIANT) As Long
'* @brief (未実装)列挙シーケンスを最初にリセットします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @param[out] pEnum 複製したCOMオブジェクトへのポインタ
'* @return 関数が成功した場合はS_OK、メモリ不足により失敗した場合はE_OUTOFMEMORYを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-ienumvariant-clone
'*
Private Function IEnumVARIANT_Clone(ByRef this As SEnumVARIANT, ByRef pEnum As LongPtr) As Long
    IEnumVARIANT_Clone = E_OUTOFMEMORY
End Function

標準モジュールを新規追加し、名前をMArrayEnumeratorに変更して、下記のコードを貼り付けます。

' [VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応) 
' Copyright (c) 2020-2024  黒箱 
' This software is released under the GPLv3. 
' このソフトウェアはGNU GPLv3の下でリリースされています。 

'/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
' @file           MArrayEnumerator
' @brief          Variant型配列用のIEnumVARIANTを生成するための関数
' @date           2020.11.15
' $Version:       1
' $Revision:      0
' @note           カスタムクラス設計者はNew_VariantArrayEnumerator()を呼び出すだけ列挙子を生成できます。
'
'/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


Option Explicit

Private Declare PtrSafe Sub CoTaskMemFree Lib "ole32" (ByVal pv As LongPtr)
#If Win64 Then
'    '下記APIが見付からないためコメントアウト
'    Public Declare PtrSafe Function InterlockedIncrement64 Lib "kernel32" (ByRef lpAddend As LongLong) As LongLong
'    Public Declare PtrSafe Function InterlockedDecrement64 Lib "kernel32" (ByRef lpAddend As LongLong) As LongLong
#Else
    Private Declare PtrSafe Function InterlockedIncrement Lib "kernel32" (ByRef Addend As Long) As Long
    Private Declare PtrSafe Function InterlockedDecrement Lib "kernel32" (ByRef Addend As Long) As Long
#End If


Private Const NUL As LongPtr = 0

'EnumVARIANTのメンバ
Private Type SEnumVARIANT
    VTablePtr    As LongPtr
    References   As LongPtr
    Elements()   As Variant
    CurrentIndex As Long
    FirstIndex   As Long
    LastIndex    As Long
End Type


'VTable
Private VTable(6) As LongPtr


'* @fn Private Sub InitVTable()
'* @brief IEnumVariant用の仮想関数テーブルを初期化します。
'*
Private Sub InitVTable()
    VTable(0) = GetAddressOf(AddressOf IUnknown_QueryInterface)
    VTable(1) = GetAddressOf(AddressOf IUnknown_AddRef)
    VTable(2) = GetAddressOf(AddressOf IUnknown_Release)
    VTable(3) = GetAddressOf(AddressOf IEnumVARIANT_Next)
    VTable(4) = GetAddressOf(AddressOf IEnumVARIANT_Skip)
    VTable(5) = GetAddressOf(AddressOf IEnumVARIANT_Reset)
    VTable(6) = GetAddressOf(AddressOf IEnumVARIANT_Clone)
End Sub


'* @fn Public Function New_VariantArrayEnumerator(ByRef Elements() As Variant, Optional ByVal FirstIndex As Variant, Optional ByVal LastIndex As Variant) As IEnumVARIANT
'* @brief ForEach内でVariant型配列を列挙可能なCOMオブジェクトを生成します。
'* @param[in] Elements ForEachで列挙する配列を指定します。
'* @param[in] FirstIndex 列挙する要素の最初のインデックスを指定します。省略するとLBound(Elements)が指定されます。
'* @param[in] LastIndex 列挙する要素の最後のインデックスを指定します。省略するとUBound(Elements)が指定されます。
'* @return IEnumVARIANTインターフェイスを持つCOMオブジェクトを返します。
'*
Public Function New_VariantArrayEnumerator(ByRef Elements() As Variant, Optional ByVal FirstIndex As Variant, Optional ByVal LastIndex As Variant) As IEnumVARIANT
    If (VTable(0) = NUL) Then Call InitVTable
    
    If (IsMissing(FirstIndex)) Then FirstIndex = LBound(Elements)
    If (IsMissing(LastIndex)) Then LastIndex = UBound(Elements)
    
    Dim this As SEnumVARIANT
    With this
        .VTablePtr = VarPtr(VTable(0))
        .References = 1
        .Elements = Elements
        .FirstIndex = CLng(FirstIndex)
        .CurrentIndex = .FirstIndex
        .LastIndex = CLng(LastIndex)
    End With
    
    Set New_VariantArrayEnumerator = CreateComEnumerator(VarPtr(this), LenB(this))


End Function


'* @fn Private Function IUnknown_QueryInterface(ByRef this As SEnumVARIANT, ByVal RIid As LongPtr, ByVal ppvObject As LongPtr) As Long
'* @brief COMオブジェクト対してインターフェイスの照会を行います。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @param[in] RIid 照会されているインターフェースのIIDへの参照。
'* @param[in] ppvObject riidパラメーターで指定されたIIDを持つインターフェースへのポインターのアドレス。
'* @return ppvObjectがNULLならE_POINTER、照会されているインタフェースを実装している場合はS_OK、それ以外の場合はE_NOINTERFACEを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void)
'*
Private Function IUnknown_QueryInterface(ByRef this As SEnumVARIANT, ByVal RIid As LongPtr, ByVal ppvObject As LongPtr) As Long
    If ppvObject = NUL Then
        IUnknown_QueryInterface = E_POINTER
        Exit Function
    End If

    If Not (RIidIsIEnumerator(RIid)) Then
        IUnknown_QueryInterface = E_NOINTERFACE
        Exit Function
    End If

    UnsafeSet(ppvObject) = VarPtr(this)
    Call IUnknown_AddRef(this)
    IUnknown_QueryInterface = S_OK

End Function


'* @fn Private Function IUnknown_AddRef(ByRef this As SEnumVARIANT) As LongLong
'* @brief COMオブジェクトへのインターフェイスポインタの参照カウントをインクリメントします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @return 新しい参照カウントを返します。この値は、テスト目的でのみ使用することを目的としています。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-addref
'*
#If Win64 Then
    Private Function IUnknown_AddRef(ByRef this As SEnumVARIANT) As LongLong
        'InterlockedIncrement64がkernel32.dllで見付からないため、スレッドアンセーフ状態で参照カウントをインクリメント
        'IUnknown_AddRef = InterlockedIncrement64(this.References)
        this.References = this.References + 1
        IUnknown_AddRef = this.References
    End Function
    
#Else
    Private Function IUnknown_AddRef(ByRef this As SEnumVARIANT) As Long
        'スレッドセーフ状態で参照カウントをインクリメント
        IUnknown_AddRef = InterlockedIncrement(this.References)
    End Function
#End If


'* @fn Private Function IUnknown_Release(ByRef this As SEnumVARIANT) As LongLong
'* @brief COMオブジェクトのインターフェイスの参照カウントを減らします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @return 新しい参照カウントを返します。この値は、テスト目的でのみ使用することを目的としています。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-Release
'*
#If Win64 Then
    Private Function IUnknown_Release(ByRef this As SEnumVARIANT) As LongLong
    
        'InterlockedDecrement64がkernel32.dllで見付からないため、スレッドアンセーフ状態で参照カウントをデクリメント
        'IUnknown_Release = InterlockedDecrement64(this.References)
        this.References = this.References - 1
        IUnknown_Release = this.References
        
        '参照カウントが0になったらインスタンスのメモリを解放
        If (IUnknown_Release = 0) Then
            Erase this.Elements
            Call CoTaskMemFree(VarPtr(this))
        End If
        
    End Function

#Else
    Private Function IUnknown_Release(ByRef this As SEnumVARIANT) As Long
        
        'スレッドセーフ状態で参照カウントをデクリメント
        IUnknown_Release = InterlockedDecrement(this.References)
        
        '参照カウントが0になったらインスタンスのメモリを解放
        If (IUnknown_Release = 0) Then
            Erase this.Elements
            Call CoTaskMemFree(VarPtr(this))
        End If
        
    End Function

#End If


'* @fn Private Function IEnumVARIANT_Next(ByRef this As SEnumVARIANT, ByVal celt As Long, ByRef rgVar As EnumVariables, ByRef pCeltFetched As Long) As Long
'* @brief 列挙シーケンスで指定された要素を取得します。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @param[in] celt 取得する要素の数
'* @param[out] rgVar 取得した要素を返す配列(をメンバに持つ構造体)。
'* @param[out] pCeltFetched NULL参照でない場合、rgVarで返した要素の数を返します。
'* @return 返される要素の数がceltと同じ場合はS_OK、返される要素の数がcelt未満の場合はS_FALSEを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-ienumvariant-next
'*
Private Function IEnumVARIANT_Next(ByRef this As SEnumVARIANT, ByVal celt As Long, ByRef rgVar As EnumVariables, ByRef pCeltFetched As Long) As Long
    
    If (VarPtr(rgVar.Element(0)) = NUL) Then
        IEnumVARIANT_Next = E_POINTER
        Exit Function
    End If

    Dim Fetched As Long
    Do While (this.CurrentIndex <= this.LastIndex)
    
        If (IsObject(this.Elements(this.CurrentIndex))) Then
            Set rgVar.Element(Fetched) = this.Elements(this.CurrentIndex)
        Else
            rgVar.Element(Fetched) = this.Elements(this.CurrentIndex)
        End If
        
        this.CurrentIndex = this.CurrentIndex + 1
        Fetched = Fetched + 1
        
        If (Fetched = celt) Then
            If VarPtr(pCeltFetched) Then pCeltFetched = Fetched
           IEnumVARIANT_Next = S_OK
            Exit Function
        End If
    Loop

    If VarPtr(pCeltFetched) Then pCeltFetched = Fetched
    
    IEnumVARIANT_Next = S_FALSE

End Function


'* @fn Private Function IEnumVARIANT_Skip(ByRef this As SEnumVARIANT, ByVal celt As Long) As Long
'* @brief 列挙シーケンス内の要素をスキップしようとします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @param[in] celt スキップする要素の数
'* @return 指定された要素数をスキップした場合はS_OK、要求された数の要素をスキップする前に最後の要素に到達した場合はS_FALSEを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-ienumvariant-skip
'*
Private Function IEnumVARIANT_Skip(ByRef this As SEnumVARIANT, ByVal celt As Long) As Long
    this.CurrentIndex = this.CurrentIndex + celt
    IEnumVARIANT_Skip = S_OK
End Function


'* @fn Private Function IEnumVARIANT_Reset(ByRef this As SEnumVARIANT) As Long
'* @brief 列挙シーケンスを最初にリセットします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @return 関数が成功した場合はS_OK、失敗した場合はS_FALSEを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-ienumvariant-reset
'*
Private Function IEnumVARIANT_Reset(ByRef this As SEnumVARIANT) As Long
    this.CurrentIndex = this.FirstIndex
    IEnumVARIANT_Reset = S_OK
End Function


'* @fn Private Function IEnumVARIANT_Reset(ByRef this As SEnumVARIANT) As Long
'* @brief (未実装)列挙シーケンスを最初にリセットします。
'* @param[in] this COMオブジェクトへのthisポインタ
'* @param[out] pEnum 複製したCOMオブジェクトへのポインタ
'* @return 関数が成功した場合はS_OK、メモリ不足により失敗した場合はE_OUTOFMEMORYを返します。
'* @details https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-ienumvariant-clone
'*
Private Function IEnumVARIANT_Clone(ByRef this As SEnumVARIANT, ByRef pEnum As LongPtr) As Long
    IEnumVARIANT_Clone = E_OUTOFMEMORY
End Function

For Eachに指定可能なクラスの作成

IIteratorインターフェイスを使用した実装

クラスモジュールを新規追加し、名前をIIteratorに変更して、下記のコードを貼り付けます。このクラス(インターフェイス)は、他の自作クラスにImplementsし、For Eachでどのようにデータを列挙するかを定義するために使用します。

' [VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応) 
' Copyright (c) 2020-2024  黒箱 
' This software is released under the GPLv3. 
' このソフトウェアはGNU GPLv3の下でリリースされています。 

'/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
' @file           IIterator
' @brief          New_Enumerator()に渡すクラスが実装すべきインターフェイスを定義します。
' @date           2020.11.15
' $Version:       1
' $Revision:      0
' @note           なし
'
'///////////
Option Explicit


'* @fn Public Function GetNext(ByRef e As Variant) As Boolean
'* @brief 次の要素を取得します。
'* @param[out] e 取得した要素を格納します。
'* @return 次の要素を取得したらTrue、次の要素を取得できなかった場合はFalseを返します。
'* @details
'*
Public Function GetNext(ByRef e As Variant) As Boolean
End Function


'* @fn Public Function Reset() As Boolean
'* @brief 列挙子を初期状態に戻します(おそらくFor Eachから呼び出されることはありません)
'* @return 関数が成功したらTrue、それ以外はFalseを返します。
'*
Public Function Reset() As Boolean
End Function

では、実際にImplementsしてみましょう。クラスモジュールを新規追加し、名前をStringIteratorに変更して、下記のコードを貼り付けます。2行目の「Implements IIterator」で、先程定義したインターフェイスをImplementsしています。IIterator_GetNext()はForEachのループ1回毎に呼び出されます。IIterator_GetNext()がTrueを返す間はループを回り続け、Falseを返したタイミングでループを抜けます。なお、IIterator_Reset()はForEachから呼び出されることは無いと思いますので、実装しなくても動作に影響はありませんが、可能であれば実装しておきましょう。

' [VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応) 
' Copyright (c) 2020-2024  黒箱 
' This software is released under the GPLv3. 
' このソフトウェアはGNU GPLv3の下でリリースされています。 

Option Explicit
Implements IIterator

Private s As String
Private i As Long
Private Length As Long

'コンストラクタ
Friend Sub Construct(Target As String)
    s = Target
    i = 1
    Length = Len(s)
End Sub

Private Function IIterator_GetNext(e As Variant) As Boolean
        
    '最後の要素を返した後に、再びGetNext()が呼ばれたタイミングでFor Each終了
    If (Length < i) Then
        IIterator_GetNext = False '要素の取得に失敗したことを通知
        Exit Function
    End If
    
    '1文字返す
    e = Mid(s, i, 1)
    i = i + 1
    
    IIterator_GetNext = True '要素の取得に成功したことを通知
    
End Function

Private Function IIterator_Reset() As Boolean
    '要素の位置を先頭に戻す
    i = 1
    IIterator_Reset = True
End Function

それでは、StringIteratorを使用して任意のクラスをForEachに対応させてましょう。

クラスモジュールを新規追加し、名前をMyStringに変更して、下記のコードを貼り付けます。なお、7行目及び17~18行目がエラーとなりますが、無視してコードを保存します。次に、MyStringクラスモジュールを解放します。解放する際にエクスポートするか確認されるため、適当な場所にエクスポートして下さい。エクスポートしたファイルを再び同じブックにインポートして下さい。先程エラーが発生していた行が消えていれば成功です。

' [VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応) 
' Copyright (c) 2020-2024  黒箱 
' This software is released under the GPLv3. 
' このソフトウェアはGNU GPLv3の下でリリースされています。 

Option Explicit

Private s As String

'デフォルトプロパティ
Public Property Get Str() As String
Attribute Value.VB_UserMemId = 0
'Attribute Value.VB_UserMemId = 0
    Str = s
End Property

Public Property Let Str(RHS As String)
    s = RHS
End Property

Public Function NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
'Attribute NewEnum.VB_UserMemId = -4
'Attribute NewEnum.VB_MemberFlags = "40"
    
    Dim ite As New StringIterator
    Call ite.Construct(s)
    
    Set NewEnum = New_Enumerator(ite)
End Function

いよいよ、カスタムクラスFor Eachで列挙してみます。標準モジュールを新規追加し、名前をEnumTestに変更して、下記のコードを貼り付けます。MyStringTest()内でF5を押してコードを実行すると、イミディエイトウィンドウに指定した文字列が1文字ずつ表示されるかと思います。

' [VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応) 
' Copyright (c) 2020-2024  黒箱 
' This software is released under the GPLv3. 
' このソフトウェアはGNU GPLv3の下でリリースされています。 

'カスタム列挙子のテスト用関数です。実行するとMyString内のメンバ変数の文字列を1文字ずつ列挙します。
Private Sub MyStringTest()

    Dim s As New MyString
    s = "abc壱弐参"
    
    Dim i As Long
    Dim e As Variant
    For Each e In s
        i = i + 1
        Debug.Print i & ":" & e
    Next

End Sub

クラス内の配列を列挙する実装

IIteratorインターフェイスによる実装は自由度が高いため、本来はこれ1つあれば全てのデータ構造に対応可能です。しかし、余分なクラスを定義せずに手軽にFor Eachに対応したいというニーズのために、PrivateなVariant型配列をIIteratorインターフェイスなしで回せるコードを書いておきました。最初の方で作成したMArrayEnumeratorがそれです。

クラスモジュールを新規追加し、名前をMyArrayに変更して、下記のコードを貼り付けます。なお、7行目及び28~29行目がエラーとなりますが、無視してコードを保存します。次に、MyArrayクラスモジュールを解放します。解放する際にエクスポートするか確認されるため、適当な場所にエクスポートして下さい。エクスポートしたファイルを再び同じブックにインポートして下さい。先程エラーが発生していた行が消えていれば成功です。

' [VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応) 
' Copyright (c) 2020-2024  黒箱 
' This software is released under the GPLv3. 
' このソフトウェアはGNU GPLv3の下でリリースされています。 

Option Explicit

Private ary() As Variant

'デフォルトプロパティ
Public Property Get Item(ByVal i As Long) As Variant
Attribute Value.VB_UserMemId = 0
'Attribute Value.VB_UserMemId = 0
    If (IsObject(ary(i))) Then
        Set Item = ary(i)
    Else
        Item = ary(i)
    End If
    
End Property

Public Property Let Item(ByVal i As Long, RHS As Variant)
    ary(i) = RHS
End Property

Public Property Set Item(ByVal i As Long, RHS As Variant)
    Set ary(i) = RHS
End Property


'列挙子を生成
Public Function NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
'Attribute NewEnum.VB_UserMemId = -4
'Attribute NewEnum.VB_MemberFlags = "40"
    Set NewEnum = New_VariantArrayEnumerator(ary)
End Function


'要素数を返す
Public Property Get Count() As Long
    Count = UBound(ary)
End Property

'要素数を変更
Public Sub Resize(ByVal Size As Long)
    ReDim Preserve ary(1 To Size)
End Sub


'コンストラクタ
Private Sub Class_Initialize()
    ReDim ary(1 To 1)
End Sub

MyArrayクラスのインスタンをFor Eachで列挙してみます。EnumTest標準モジュールに下記のコードを追加で貼り付けます。MyArrayTest()内でF5を押してコードを実行すると、イミディエイトウィンドウにMyClass内の要素が1つずつ表示されれば成功です。

' [VBA]VBA単独でFor Eachに対応した自作クラスを作成する方法(64bit Excel対応) 
' Copyright (c) 2020-2024  黒箱 
' This software is released under the GPLv3. 
' このソフトウェアはGNU GPLv3の下でリリースされています。 


'配列用のカスタム列挙子のテスト用関数です。実行するとMyArrayのプライベート配列を列挙します。
Private Sub MyArrayTest()

    Dim c As New MyArray
    Dim e As Variant
    
    Call c.Resize(9)
    Dim i As Long
    For i = 1 To 9
        c(i) = i & ":" & Rnd()
    Next
    

    For Each e In c
        Debug.Print e
    Next
    
End Sub

おわりに

いかがだったでしょうか?VBA開発が中規模以上になってくると、複雑なデータ構造を取り扱う場面が多くなってくると思います(VBAで規模の大きい開発をするなというツッコミはなしで)。この際、ループ構造が2重、3重になってくるとコードの可読性が低下し、保守性が悪くなってきます。そんな時、データ構造をクラス内に隠蔽したいところですが、VBAの標準機能ではカスタムクラスをForEachで回せないため、敢え無く失敗してしまいます(インターフェイスかCallByNameで内部イテレータという手段もありますが)。そのような辛酸を舐め尽くした全てのVBAプログラマのために、本記事を執筆しました。

なお、今回紹介したコードの内、ForEachの実装に最低限必要なモジュールは下記の通りです。

  • MEnumerator
  • MEnumeratorBase
  • IIterator

MArrayEnumeratorは、必要に応じて追加して下さい。

繰り返しになりますが、ウィルス対策ソフトの種類によってはForEachが遅くなりますので注意して下さい。

さて、そろそろ64bit版Excelへ移行するニーズもあるかと思い、今回はExcel2010以降の32bit/64bit共用のコードを書きました。1点気掛かりなのが、64bit版コードはCOMの参照カウント増減がスレッドセーフではありません。というのも、InterlockedIncrement64がWindowsRTにしか対応していないようで、かつInterlockedIncrementが64bit版kernel32.dllに含まれていませんでした。まぁ、メモリ上に作成した(シングルスレッドの)VBA上でのみ走っているCOMオブジェクトに、スレッドセーフ対応が必要か疑問が残る所なので、実害は無いでしょう。しかし32bit版と64bit版で実装が違うのは気持ち悪いので、解決方法を見付けたら直します。何か情報をお持ちお方、ご連絡いただけると幸いです。

プログラムの利用について

本プログラムのライセンスは「GPL-3.0(GNU General Public License version 3)」を適用しています。

本プログラムを私的に利用(個人利用、並びに企業またはその他の組織の内部利用)される方は、本プログラムを無償で利用できると考えて差し支え御座いません。その場合でも、本プログラム内の著作権及びライセンスの表示は削除せずに表示しておいて下さい。

その他の方は、GNU 一般公衆利用許諾書(GNU GENERAL PUBLIC LICENSE)バージョン3の内容をご理解頂き、適正な利用をお願い致します。同ライセンス以外での利用をご希望の方はお問い合わせフォームよりご連絡下さい。

本ホームページのプログラムを書籍またはホームページ等で一般公開したい方は、お問い合わせフォームよりご連絡下さい。

スポンサーリンク

VBAGPLv3,ソースコード

Posted by 黒箱