- VBA で Sub や Function に引数を渡す場合、他の多くの言語同様、値渡しと参照渡しを使い分けられるけど、整数とかの単純な値型ならともかく、文字列や配列、オブジェクト型はどっちで渡すのが正解なの?
えっ参照渡しがデフォルトなんすか?
引数の値渡しや参照渡しがなんなのかというレベルの話はもっと親切なサイトを見てもらうとして、VBA では以下のように引数の前に ByVal
または ByRef
と書くことでその引数が値渡しか参照渡しかを指定 できる。
'値渡しの例
Sub ByValSample(ByVal arg As Integer)
'引数を書き換えても、呼び出し元の変数は変わらない
arg = 2
End Sub
'参照渡しの例
Sub ByRefSample(ByRef arg As Integer)
'引数を書き換えると、呼び出し元の変数も書き換わる
arg = 2
End Sub
で、C言語とかで慣れてる人からするとビックリするんだけど、VBA では ByVal / ByRef
の指定を省略した場合、デフォルトは ByRef
(参照渡し) になる。つまりは Sub 内で引数の値を書き換えると、呼び出し元の変数が書き換わる。
他の多くの言語の常識では、参照渡しといったら戻り値以外にも複数の値を返したい場合にだけ使う物なので、全引数が参照渡しと言われても生理的にキモイし明示的に ByVal
と書きたくなってしまうんだけど、なんで VBA ではこういう仕様にしちゃったのかな?、と真面目に考えてみた。
VBA の文字列型と配列型
VBA のオブジェクト型は内部的にはポインタ(オブジェクトのアドレス)なので、他の変数に代入した場合もどちらも同じオブジェクトを指す。
'オブジェクト型の例
Dim listA As New Collection
Dim listB As Collection
listA.Add 10
'オブジェクトのアドレスを代入しているだけ
Set listB = listA
listA.Add 20
'listA と listB は同じ物を参照してるので、listB(2) も Add 20 されてる
Debug.Print listB(2) '20 と表示
一方、配列型や文字列型はオブジェクト型ではなく値型 なので、他の変数に代入した場合は複製される。
'配列型の例
Dim arrayA(2) As Integer
Dim arrayB
arrayA(1) = 50
'代入した時点で arrayA の全要素が arrayB に複製される
arrayB = arrayA
arrayA(1) = 100
'arrayA と arrayB は別物なので、arrayB(1) は複製前に入れた 50 のまま
Debug.Print arrayB(1) '50 と表示
'文字列型の例
Dim strA as String
Dim strB as String
strA = "あいう"
'参照を代入してるようにも見えるが、この時点で別の文字列ワークが作られて複製される模様
strB = strA
'strA と strB の実体は別のアドレスになっている
'StrPtr() で文字列ワークのアドレスが分かる
Debug.Print StrPtr(strA)
Debug.Print StrPtr(strB)
ということで、Sub や Function に引数を値渡しする際にも同じ事が起こるので、少なくとも配列型や文字列型に関しては、ByVal
で渡すと複製を作る余計な処理が増えるので、ByRef
で渡した方がよさそう。デフォルトが ByRef
になってるのはこのせいなのかね。
一方、オブジェクト型については、別に ByVal
で渡してもオブジェクトのアドレスが渡されるだけなので、複製が作られたりはしない。
あとは好みの問題
てことでまとめると、配列型と文字列型に関しては ByVal
で渡すと明確なデメリットがあるので ByRef
の方がいいとして、あとはぶっちゃけ好みの問題 なんですよな。
もちろん全引数に厳格に明記するスタンスでもいいのだけれど、ByVal
と ByRef
って一目で判別しにくくて全引数に書かれると正直鬱陶しいというのもあるので、「戻り値用途以外では引数の値は書き換えない」というルールにしたうえで、ByVal / ByRef
は基本的に省略する というのもそれはそれで読みやすい気はする。
あとは個人的には 戻り値用途の引数は一目で分かるようにしたい ので、それだけは敢えて ByRef
と書くようにするとか?
オマケ:VBA にもずいぶん泥臭い機能が用意されてんのね
それにしても、上のコードでちょっと使ったけど、文字列型の実体のアドレスを返すなんて泥臭いものが VBA に用意されてるとは思わなかったよ。他にもオブジェクト型の実体や、変数自体のアドレスを返す関数なんてのもある。
'オブジェクトの場合
'オブジェクト型以外で ObjPtr を使うとエラー
Dim varObj As New Collection
Debug.Print VarPtr(varObj) '変数のアドレス
Debug.Print ObjPtr(varObj) 'オブジェクトのアドレス(未設定だと0)
'文字列の場合
'文字列型以外で StrPtr を使うとエラー
Dim varStr As String
varStr = "あいう"
Debug.Print VarPtr(varStr) '変数のアドレス
Debug.Print StrPtr(varStr) '文字列ワークのアドレス(未設定だと0)
なんか調べ始めたきっかけは一緒なのに、最終的には明後日の方に行ってしまった方が居て面白かったので最後にオマケに貼っときます。そんなことできるんか VBA。