概要
Visual Basic for Application (VBA) において、手続きをパラメータとして引き渡す方法、いわゆるコールバック関数の実装方法を解説します。
クラスメソッドを作る
VBAでは手続きをポインタなどで扱う仕組みがありません。そこでまず、クラスモジュールにメソッドを定義してやり、インスタンスオブジェクトからメソッドをコールするという手順でコールバック関数を実装することにします。
Option Explicit
Sub callSub(ParamArray prms())
    ' メソッドの名前
    Debug.Print "Callback1.callSub"
    ' 引数の一覧
    Dim it, prmsStr As String
    prmsStr = "  prms = "
    For Each it In prms
        prmsStr = prmsStr & it & ", "
    Next
    Debug.Print prmsStr
End Sub
Option Explicit
Sub test()
    Dim cbObj As New Callback1
    executeCallback cbObj
End Sub
Sub executeCallback(callbackObj)
    ' Variant型から"callSub"を呼び出す
    callbackObj.callSub 1, "α"
End Sub
VBEのイミディエイトウィンドウでModule1.testを実行した結果。
Module1.test
Callback1.callSub
  prms = 1, α, 
CallByName命令を使う
前節の例では、executeCallbackに渡されたオブジェクトに対してメソッド名を決め打ちで呼び出すことしか出来ませんが、CallByName命令を使うと、オブジェクトとメソッド名の両方をパラメータとして渡すことができます。
Option Explicit
Sub test()
    Dim cbObj As New Callback1
    executeCallback Array(cbObj, "callSub")
End Sub
Sub executeCallback(callbackObj)
    ' 配列からオブジェクトと、メソッド名を取り出して呼び出す
    CallByName callbackObj(0), callbackObj(1), VbMethod, 2, "β"
End Sub
Module2.test
Callback1.callSub
  prms = 2, β, 
インタフェースクラスを用いる
VBAはオブジェクト指向のインタフェースの仕組みをサポートしているため、これを用いてタイプセーフなコールバックを実装することもできます。
まず、インタフェースクラスとしてICallbackを作成し、メソッド名のみを定義します。
Option Explicit
Sub callSub(ParamArray prms())
End Sub
インタフェースクラスの継承にはImplementsステートメントを使います。callSubの定義をCallback2へ実装した例を下記に示します。
Option Explicit
Implements ICallback
Private Sub ICallback_callSub(ParamArray prms())
    ' メソッドの名前
    Debug.Print "Callback2.ICallback_callSub"
    
    ' 引数の一覧
    Dim it, prmsStr As String
    prmsStr = "  prms = "
    For Each it In prms
        prmsStr = prmsStr & it & ", "
    Next
    Debug.Print prmsStr
End Sub
インタフェースのメソッドをオーバーライドするにはImplementsしたクラス名と、メソッド名をアンダーバー(_)で組み合わせて下記のように定義する必要があります。
クラス名_メソッド名
Callback2はICallbackを継承しているため、Callback2型のオブジェクトをICallback型の仮引数に渡しても、型チェックに合格します。
Option Explicit
Sub test()
    Dim cbObj As New Callback2
    executeCallback cbObj
End Sub
Sub executeCallback(callbackObj As ICallback)
    ' ICallback型オブジェクトから"callSub"を呼び出す
    callbackObj.callSub 3, "γ"
End Sub
Module3.test
Callback2.ICallback_callSub
  prms = 3, γ, 
オーバーライドしたメソッド名を隠蔽する
Callback2の定義は公開メソッド名がICallback_callSubとなり不格好なので、これを隠蔽して、callSubを公開メソッド名となるように改良します。
Option Explicit
Implements ICallback
' ICallback型のプライベート関数
Private Sub ICallback_callSub(ParamArray prms())
    Debug.Print "Callback3.ICallback_callSub"
    substance prms
End Sub
' Callback3型のグローバル関数
Public Sub callSub(ParamArray prms())
    Debug.Print "Callback3.callSub"
    substance prms
End Sub
' 処理の実体
Private Sub substance(ByVal prms)
    ' メソッドの名前
    Debug.Print "Callback3.substance"
    
    ' 引数の一覧
    Dim it, prmsStr As String
    prmsStr = "  prms = "
    For Each it In prms
        prmsStr = prmsStr & it & ", "
    Next
    Debug.Print prmsStr
End Sub
オーバーライドしたICallback_callSubメソッドをPrivate指定で隠蔽し、代わりにcallSubメソッドをPublic指定で公開しています。こうするとCallback3型のオブジェクトからはcallSubしか公開されず、更に同じインスタンスでもICallback型にキャストされるとちゃんとICallback_callSubを呼び出すことができます。
Option Explicit
Sub test()
    Dim cbObj As New Callback3
    executeCallback cbObj
    
    ' Callback3型から"callSub"を呼び出す
    cbObj.callSub 4.2, "δ-2"
End Sub
Sub executeCallback(callbackObj As ICallback)
    ' ICallback型から"callSub"を呼び出す
    callbackObj.callSub 4.1, "δ-1"
End Sub
Module4.test
Callback3.ICallback_callSub
Callback3.substance
  prms = 4.1, δ-1, 
Callback3.callSub
Callback3.substance
  prms = 4.2, δ-2,