t-hom’s diary

主にVBAネタを扱っているブログです。

VBA なぜ引数が一つのときは、カッコを付けても付けなくてもプロシージャを呼び出せるのか

初心者の方はプロシージャ呼び出しの時にカッコを付けるかどうか迷う方もいると思う。

基本ルールは、Callを書く場合と、戻り値を利用する場合にカッコを付け、それ以外では付けてはいけないである。

以下のように、不要なカッコを付けると、コンパイラに叱られてしまう。

f:id:t-hom:20160611064920p:plain

しかし困ったことに、この基本ルールには例外がある。

引数がひとつなら、単体でカッコを付けても呼び出せてしまうのだ。

たとえば以下のようなケースである。

Sub Sample()
    x = 2
    Twice (x)
End Sub

Sub Twice(a)
    MsgBox a * 2
End Sub

基本ルールに則るとTwiceプロシージャを呼び出すのに、「Twice x」とカッコ無しで書かないといけないのではないか。

人に教えるときに非常にやりづらい。まったく、困った例外を作ってくれたものだ。

…と、最近まで考えていたのだが、実は単純に例外ルールというわけでもないようだ。

実は引数が二つ以上でも、個別の引数に対してカッコを付けることはできる。

こんな風に。

Sub Sample()
    x = 2
    y = 3
    Add (x), (y)
End Sub

Sub Add(a, b)
    MsgBox a + b
End Sub

つまり、前述のTwice (x)は、個別の引数xに対するカッコであり、引数全体を囲むカッコではないのだ。

呼び出しのパターン

以下にカッコの有無でパターンを洗い出してみた。

【個別引数にカッコをつけて呼び出し】

[1] プロシージャ名 (引数1)
[2] プロシージャ名 (引数1), (引数2)
[3] プロシージャ名 (引数1), (引数2), (引数3)

【個別引数にカッコをつけずに呼び出し】

[4] プロシージャ名 引数1
[5] プロシージャ名 引数1, 引数2
[6] プロシージャ名 引数1, 引数2, 引数3

【引数全体にカッコをつけて呼び出し】

[7] Call プロシージャ名(引数1)
[8] Call プロシージャ名(引数1, 引数2)
[9] Call プロシージャ名(引数1, 引数2, 引数3)

【個別引数と、引数全体にカッコをつけて呼び出し】

[10] Call プロシージャ名 ((引数1))
[11] Call プロシージャ名 ((引数1), (引数2))
[12] Call プロシージャ名 ((引数1), (引数2), (引数3))

【NGパターン】

[13] プロシージャ名 (引数1, 引数2)
[14] Call プロシージャ名 引数1, 引数2

こうして並べてみると、例外などではなく、一本きれいなルールが通っているのがわかる。

全体カッコの意味

以下のように全体カッコを外して書いても文法上は一見問題なさそうに見える。

Call プロシージャ名 引数1, 引数2, 引数3

ではVBAコンパイラはなぜこれをエラーとみなすのだろうか。
Callだと問題が見えてこないので、基本ルールの「戻り値を利用する場合」で検証してみよう。

以下の命令をどう解釈すべきだろうか。

プロシージャA プロシージャB 引数1, 引数2, 引数3

上記のようにカッコがない場合、解釈が次の3通りに分かれてしまう。

[1] プロシージャA プロシージャB(引数1), 引数2, 引数3
[2] プロシージャA プロシージャB(引数1, 引数2), 引数3
[3] プロシージャA プロシージャB(引数1, 引数2, 引数3)

引数全体にカッコを付けるのは「ここまでがプロシージャXの引数なので、先に評価してくださいね」とコンパイラに教えてあげる意味があるのだ。

Callの場合は人が見たら問題なさそうに見えるが、コンパイラはプロシージャの引数の範囲を柔軟に判定してくれるということはないので、やはりカッコが必要である。

個別カッコの意味

個別の引数につけるカッコにも非常に重要な意味がある。
VBAはデフォルトでは、プロシージャへの引数を参照渡しする。

下記のようにカッコなしでTwice aと書くと、変数aの参照が渡され、それが2倍されるので、最終的に20が表示される。

Sub Sample()
    a = 10
    Twice a
    MsgBox a
End Sub

Sub Twice(x)
    x = x * 2
End Sub


ところが、個別のカッコをつけると引数が値渡しになるのだ。
次のコードではTwiceへは単に10が渡されるので、Twiceプロシージャでそれを2倍したところでSampleプロシージャの変数aは相変わらず10のままである。

Sub Sample()
    a = 10
    Twice (a)
    MsgBox a
End Sub

Sub Twice(x)
    x = x * 2
End Sub

ただし、Call Twice(a)とすると、そのカッコは引数全体を囲むカッコとみなされ、aは参照渡しになる。

Sub Sample()
    a = 10
    Call Twice(a)
    MsgBox a
End Sub

Sub Twice(x)
    x = x * 2
End Sub

さらにカッコで囲むと、外側のカッコは引数全体のカッコ、内側のカッコは値渡しを表す個別カッコになる。

Sub Sample()
    a = 10
    Call Twice((a))
    MsgBox a
End Sub

Sub Twice(x)
    x = x * 2
End Sub

整理すると、以下の3パターンでは、[1]と[2]は参照渡しになるが、[3]は値渡しになってしまう。
[1] プロシージャ a
[2] Call プロシージャ(a)
[3] プロシージャ (a)

パターン[3]について、「例外として引数が一つの場合はカッコを付けることができる」という理解では、思わぬバグにつながることがあるということだ。

なぜ個別カッコは値渡しなのか

ふつう、値渡しをするには呼び出されるプロシージャ側でByValを使用する。
なんで個別カッコが値渡しになるという変なルールがあるのだろうか。

これはあくまで私の推測であるが、これも例外的なカッコの使い方ではなく、「カッコ内は先に評価する」という基本ルールに則って、結果的に値渡しになっているだけではないかと思う。

つまり、たとえば (5 + 2) * 3 と書いたときに5 + 2が先に評価されて7になるのと同じ理屈である。

先ほどのコードでは、(a)が評価されて10になった後に渡されるから、値渡しになる。

そう考えると不可解で例外的に思えるルールも、実は例外ではなく評価の大原則に基づいた至極当然な結果であるといえる。

あくまで推測であるが。

あとがき

プロシージャ (a)と書くと、そのカッコは引数全体のカッコではなく個別カッコなので値渡しになってしまう。
呼び出し先がByRefで待ち受けていても、値渡しになってしまう。
なぜならすでに呼び出し元で先に評価されて、値になってしまっているから。

参照渡しを想定している場合は、個別カッコを付けるとバグの元になるということをしっかり押さえておきたい。

今回の事実をテクニックとして使うのはお勧めしない。
基本は「Callを書く場合と、戻り値を利用する場合にカッコを付け、それ以外では(たとえ付けられても)付けてはいけない」という原則に忠実に、値渡しか参照渡しかは呼び出し先のプロシージャのByValまたはByRefで制御するのが良い。

VBAの書籍やネットの情報ではで引数のカッコについて細部まで説明しているものが見つけられなかった。
「例外」として説明しているか、そもそも基本ルール以外の説明がないケースが多い。

※ちゃんと探せばあるのかもしれない。

私はVBAではなく、WSHの書籍で今回の事実を知った。

WSHクイックリファレンス 第2版

WSHクイックリファレンス 第2版

さすがオライリー
ちなみに購入目的は、VBSをやりたかったわけではなく、FileSystemObjectやDictionaryなど、VBAからも便利に使用できるMicrosoft Scripting Runtime系のオブジェクト仕様を抑えておくため。

VBAのテクニック集などにも申し訳程度にFileSystemObjectの使用方法は掲載されているが、Microsoft Scripting RuntimeはWSHが本家なので、詳しく仕様を知りたいときはこちらがおすすめ。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。