ByRef As Variant とは?

(自分用メモ)

VBAByRef x As Longと宣言された仮引数にLong型以外の実引数を渡すと、「ByRef引数の型が一致しません。」というエラーになる。しかしByRef x As Variantという宣言だったら Variant型以外の実引数、たとえばLong型でも渡すことができる。Variantなんだから当然と思われるかもしれないが、変数のアドレスを確認してみると、Variantを渡したときと他の型を渡した時では状況が違う。

Sub addressCheck()
    Dim a As Long: a = 2
    Dim v As Variant: v = 5
    Debug.Print "VarPtr(a) ="; VarPtr(a)
    Debug.Print "VarPtr(v) ="; VarPtr(v)
    Debug.Print "a ="; a; "v ="; v
    addressCheck_sub a    ' 引数のアドレスを出力して書き換える
    addressCheck_sub v    ' 引数のアドレスを出力して書き換える
    Debug.Print "a ="; a; "v ="; v
End Sub

' 引数のアドレスを出力して書き換える
Private Sub addressCheck_sub(ByRef x As Variant)
    Debug.Print "VarPtr(x) ="; VarPtr(x)
    x = 10
End Sub

この Sub addressCheck() を実行すると次のように出力された。

VarPtr(a) = 93921072 
VarPtr(v) = 93921048 
a = 2 v = 5 
VarPtr(x) = 93921024  'a と違うアドレス
VarPtr(x) = 93921048  'v と同じアドレス
a = 10 v = 10         'どちらも書き換えはできている...

Variant を渡したときアドレスは一致するので x に代入することは v に代入することと同じである。Long a を渡したとき、それを受ける引数 x のアドレスは a とは異なるにもかかわらず、x に代入した結果が a にも反映された。

引数 実引数 アドレス比較 代入結果
ByRef x As Variant a As Long 不一致 変更される
ByRef x As Variant v As Variant 一致 変更される

つまり、実引数の型によって x の状態が次のようになっている。(比喩的な表現が混じっている)

引数 実引数 アドレス比較 x の状態 ( x の内部 ) x = 10 の意味
ByRef x As Variant a As Long 不一致 aへのポインタ 0 != (VT_BYREF & x.vt) 間接代入
ByRef x As Variant v As Variant 一致 vの参照 0 == (VT_BYREF & x.vt) 直接代入

代入の意味が上と下では違っているが、普通にVBAを使っている分にはその違いを意識することはない。しかし VBAHaskell の swapVariant 関数のようにメモリ上のベタコピーを行うと、その違いが浮き彫りになる。

Sub swapTest()
    Dim a As Long, b As Long
    a = 1&: b = 2&
    swapVariant a, b
    Debug.Print a; b
    
    Dim v As Variant, w As Variant
    v = 1&: w = 2&
    swapVariant v, w
    Debug.Print v; w
End Sub

この Sub swapTest() を実行すると次のように出力される。

 1  2 
 2  1 

ふたつの変数をスワップしたはずなのに、 Long a と Long b の組み合わせのときはスワップされていない。swapVariant のシグネチャFunction swapVariant(ByRef x As Variant, ByRef y As Variant) As Long であり、x や y の指す先が変わっただけなので a にも b にも影響は及ばない。

スワップ スワップ 結果
x は a へのポインタ x は b へのポインタ aは1のまま
y は b へのポインタ y は a へのポインタ bは2のまま

Variant を渡したときはスワップの対象は変数自身なのでこうなる。

スワップ スワップ 結果
x = v = 1 x = v = 2 vは1から2に変化
y = w = 2 y = w = 1 wは2から1に変化

スワップと代入を伴うパターンはこうなる。

Sub swapTest2()
    Dim a As Long, b As Long
    a = 1&: b = 2&
    swapTest_sub a, b
    Debug.Print a; b
    
    Dim v As Variant, w As Variant
    v = 1&: w = 2&
    swapTest_sub v, w
    Debug.Print v; w
End Sub

Private Sub swapTest_sub(ByRef x As Variant, ByRef y As Variant)
    swapVariant x, y
    x = 111
    y = 999
End Sub
 999  111 
 111  999