コーディング規約を自分でまとめ直そうと思って他の人のガイドラインを見ていたら、値渡しと参照渡しに関する間違いを発見したのでそれについて。ついでに、変数の値型と参照型の違いの知識も必要になるので、それについても説明していきます。
メモリーとアドレス
実行中のプログラムはメモリー上に読み込まれていますが、そのメモリーにはアドレス(番地)が割り振られています。プログラム(CPU)がメモリーからデータを読み取る際には、このアドレスを使ってデータがどこに入っているのか指定します。
アドレス | 内容 |
---|---|
xxx~xxx番地 | Windowsのデータやプログラム |
xxx~xxx番地 | プログラムA |
xxx~xxx番地 | プログラムB |
VBAの変数もメモリーに保存されていますから、アドレスが割り振られています。VBAでアドレスを意識する必要が無いのは、パソコンとプログラムコードの間にVBAやWindowsが入って、変数とアドレスとの対応を自動的に処理してくれているからです。
ですから、VBAで変数を使うときにはアドレスが割り振られていて、内部ではこのアドレスを使ってデータにアクセスをしていると覚えておいてください。
実際のアドレスを表すときは16進数を使いますが、このページで扱うのは架空のアドレスですので、10進数表記の適当な番地を使います。
アドレス | 変数名 | 型 | 変数の内容 |
---|---|---|---|
1 | i | Long | 4 |
2 | j | Long | 2 |
3 | itemCode | String | 123456789 |
4 | name | String | Book |
変数に値を持つ型と参照を持つ型
値渡しと参照渡しについて説明する前に、変数の値型と参照型の違いについて説明します。
値型の変数
値型の変数とは、変数自体に値が格納されているものの事です。VBAの本だとこちらのイメージしか書いていないものが多いですね。
アドレス | 変数名 | 型 | 変数の内容 |
---|---|---|---|
1 | itemCode | String | A001 |
2 | name | String | Metal Rack |
3 | model | String | TypeA |
4 | quantity | Long | 100 |
5 | available | Boolean | TRUE |
変数に値が入っているのは当たり前では?と思うかもしれませんが、参照型の変数はこれとは違うデータの扱い方をしています。
参照型の変数
参照型の変数とは、変数にはアドレス(参照)が保存されていて、データ自体は別の場所に保存されているもののことです。
何のことか分からないと思うので、実例を見てもらいます。エクセルブックを開いていて、ワークシートが3つある状態を想像してください。
アドレス | 変数名 | 型 | 変数の内容 |
---|---|---|---|
1 | targetBook | Workbook | 100番地への参照 |
2 | masterSheet | Worksheet | 200番地への参照 |
3 | orderSheet | Worksheet | 201番地への参照 |
100 | Workbook | ワークブック | |
200 | Worksheet | Sheet1 | |
201 | Worksheet | Sheet2 | |
202 | Worksheet | Sheet3 |
ワークブックやワークシートの実体は100番地以降に保存されていて、変数にはその参照(アドレス)のみ入っています。参照型の変数の場合、変数の中のアドレスを読み取ってから、更にそのアドレスへ実際のデータを読みに行く処理が入ります。
変数をコピーしたときの動作の違い
値型は値自体がコピーされる
値型の変数の場合、itemName1 = itemName2
の様に代入を行うと、値が丸々コピーされます。
アドレス | 変数名 | 型 | 変数の内容 |
---|---|---|---|
1 | itemName1 | String | Metal Rack |
2 | itemName2 | String | Metal Rack |
値型の変数はそれぞれ独立していますから、片方の変数の中身を変えても、他方には影響がありません。
アドレス | 変数名 | 型 | 変数の内容 |
---|---|---|---|
1 | itemName1 | String | Metal Rack |
2 | itemName2 | String | Book Shelf |
参照型は代入しても実体は1つ
先ほど出した例でset currentSheet = orderSheet
とすると、次の様になります(余分は所は再整理)。
アドレス | 変数名 | 型 | 変数の内容 |
---|---|---|---|
1 | orderSheet | Worksheet | 200番地への参照 |
2 | currentSheet | Worksheet | 200番地への参照 |
200 | Worksheet | Sheet1 |
Sheet1の実体はそのままで、Sheet1への参照が2つに増えました。
参照型の変更はコピー元にも影響がある
参照先のワークシートの実体は1つですから、ある変数から中身を操作すると、他の変数でも変更が反映されてしまいます。例えば、orderSheet.Name ="Orders"
と名前を変えると、currentSheet.Name
でも同じ名前が取得できます。
アドレス | 変数名 | 型 | 変数の内容 |
---|---|---|---|
1 | itemName1 | String | Metal Rack |
2 | itemName2 | String | Book Shelf |
値渡し(ByVal)と参照渡し(ByRef)
プログラミング言語の関数への引数の受け渡しには、「値渡し」と「参照渡し」の2種類があります。
ByVal/ByRefの使用方法
値渡しか参照渡しか指定するには、引数の名前の前にByVal/ByRefを付けます。ちなみに、このコードの場合は、どちらを使っても実行結果は変わりません。
Sub Main() Dim greeting1 As String: greeting1 = "Hello" Dim greeting2 As String: greeting2 = "World" ShowMessage greeting1, greeting2 End Sub Sub ShowMessage(ByVal Message1 As String, ByRef Message2 As String) MsgBox Message1 MsgBox Message2 End Sub
引数の値渡し(ByVal)と参照渡し(ByRef)
ByValとByRefは、受け取り側の変数が値型になるか参照型になるかが違ってきます。
具体例として、前のコードのメモリーを見てみます。
アドレス | 関数 | 変数名 | 変数の内容 |
---|---|---|---|
1 | Main | greeting1 | Hello |
2 | Main | greeting2 | World |
11 | ShowMessage | Message1 | Hello |
12 | ShowMessage | Message2 | 2番への参照 |
ByValを指定したMessage1は値がコピーされていますが、ByRefを指定したMessage2が参照型になっています。
これのことをそれぞれ値渡し(ByVal)と参照渡し(ByRef)と呼びます。
参照渡しをすると呼び出し元の変数が書き換えられる
先のコードで登場する変数は本来は値型(String型)ばかりです。関数も違うため、ShowMessageの中で変数を変更しても、Mainの中の変数には影響がないはずです。しかし、実際にはByRefを使って参照型になっているため、Message2への変更がMainへ反映されてしまいます。
例えば、次のコードの実行結果は”Hello”と”VBA”になります。”Hello”と”World”でも、”Good”と”VBA”でもありません。
Sub Main() Dim greeting1 As String: greeting1 = "Hello" Dim greeting2 As String: greeting2 = "World" ReplaceMessage greeting1, greeting2 MsgBox greeting1 ' Hello MsgBox greeting2 ' VBA End Sub Sub ReplaceMessage(ByVal Message1 As String, ByRef Message2 As String) Message1 = "Good" Message2 = "VBA" End Sub
値渡し(ByVal)と参照渡し(ByRef)の使い分け
2種類あるのは分かったけれど、どう使い分ければいいのかというお話。
ほとんどのケースでは値渡しを使うべき
原則、ByRefは使うべきではありません。バグの原因になったり、プログラムの修正が難しくなります。
いいプログラムの作り方の指針の中に「密結合より疎結合である方が良い」というものがあります。詳しくは下のサイトやネットで検索してもらうとして、簡単に言うと「部品同士が独立していて、修正や置き換えをしても互いに影響しない、再利用がしやすい。」という状態です。
ByRefを使うと呼び出し先が呼び出し元に影響を与えることになりますから、次の様な問題が発生しやすくなります。
- 仕様通り変数が使われているのか分からない
- 関数の中を見ないと変数の書き換えが起こるのか分からない
- 想定外の場所で呼び出し元の変数が書き換わっている
- 呼び出し先の関数を作り直したらなぜか動かなくなった
- 別の処理から関数を呼ぶとなぜか正しく動かない
また、後で説明しますが、VBAの場合はByRefを使っていなくても自動的に参照渡しになっているケースがかなりあります。その場合に、引数を直接書き換えてしまうと呼び出し元にも影響があるので、ByValとByRefを明示しておくことをおすすめします。
Sub Main() Dim value As Long value = 1 CountUp value ' 参照渡しになっている Debug.Print value ' メインルーチンでは値を操作していないのに2が出力される End Sub Sub CountUp(Num As Long) Num = Num + 1 End Sub
参照渡しのメリット
複数の値を返せる
Functionプロシージャで返せる値は1つだけですが、ByRefを使うと複数の値を呼び出し元に返す事ができます。
返すことはできますが、できることならクラスや構造体などをFunctionプロシージャの戻り値に使う様にしてください。
リソースを節約できる
値渡しでデータを渡すと丸々データがコピーされてしまいます。
String型の場合、最大約2GBのデータを入れることができます。そこまでいかなくても、数十~数百MBもあると、その分メモリーを使ってしまいますし、呼び出しの度にコピーをしていたら負荷もかかります。
これが参照渡しであれば、アドレスしか入っていないのでメモリーの使用量が少なく、データのコピーも発生しません。
とはいえ、VBAでそんなデータを扱う事はあまりないでしょう。プログラミングはバグの少なさ、メンテナンスのしやすさが最優先ですから、必要も無いのに速度が上がるからと言ってバグの出やすい方法を使うべきではありません。
VBAではByRefを使わなくても参照渡しになっている
関数の呼び方によって変わる値渡しと参照渡し
VBAには複数の関数の呼び方がありますが、ByVal/ByRefを明示していない場合、呼び方によって参照渡しか値渡しかが変わります。そのため、渡された引数を書き換えてしまうとなぜかメインルーチンの変数が書き換わるという事故が起きてしまいます。
Sub Main() Dim value As Long value = 1 ' ここから参照渡し CountUp value ' value = 2 Call CountUp(value) ' value = 3 ' ここから値渡し Call CountUp((value)) ' value = 3 CountUp (value) ' value = 3 CountUp ((value)) ' value = 3 End Sub Sub CountUp(Num As Long) Num = Num + 1 End Sub
この例はSubプロシージャですが、Functionプロシージャでも動きは同じです。
引数には必ずByValを付ける
解決策は単純で、ByValを付ければすべて値渡しになります。
Function CountUp2(ByVal Num As Long) Num = Num + 1 End Function
参照型変数も値渡しで渡す
ここでやっと記事を書き始めたきっかけに到達。
配列や構造体は値渡しができないのですが、オブジェクトに関して言うと、明確に動作が違います。
構造体やオブジェクトはそもそも参照渡しでしか関数に渡せないので、参照渡しとする。
Excel VBAコーディング ガイドライン案
参照渡しが問題になる例
オブジェクトは参照渡ししかできないと書いてあるので、ワークシートオブジェクトを使って反例を書いてみます。
値渡しの例
まず、値渡しを使った例です。関数内で引数をNothing
に置き換えていますが、値渡しなので呼び出し元に影響はありません。
Sub ByValMain() Dim ws As Worksheet Set ws = Worksheets(1) Debug.Print "Before:" & ws.Name ' 変数を値渡しする ByValTest ws ' 問題なく動く Debug.Print "After:" & ws.Name End Sub ' 値渡しされた内容をNothingで置き換える Sub ByValTest(ByVal Arg As Worksheet) Debug.Print "ByValTest:" & Arg.Name Set Arg = Nothing End Sub
メモリー内の内容はこんな感じになっています。
アドレス | 関数 | 変数名 | 変数の内容 |
---|---|---|---|
1 | ByValMain | ws | 100番への参照 |
2 | ByValTest | Arg | 100番への参照 |
100 | Sheet1オブジェクト |
参照渡しの例
一方、こちらは参照渡しを使った例です。参照元の変数までNothing
に置き換わっているため、Debug.Print "after:"; ws.Name
でエラーになります。
Sub ByRefMain() Dim ws As Worksheet Set ws = Worksheets(1) Debug.Print "Before:" & ws.Name ' 変数を値渡しする RefTest ws ' wsの中身がNothingになっているのでエラーになる Debug.Print "After:" & ws.Name End Sub ' 参照渡しされた内容をNothingで置き換える Sub ByRefTest(ByRef Arg As Worksheet) Debug.Print "ByRefTest:" & ws.Name Set Arg = Nothing End Sub
メモリーの内容を表にすると次の様になります。変数Argは参照型変数への参照になっています。
アドレス | 関数 | 変数名 | 変数の内容 |
---|---|---|---|
1 | ByRefMain | ws | 100番への参照 |
2 | ByRefTest | Arg | 1番への参照 |
100 | Sheet1オブジェクト |
これが、ByRefTest
の中でNothing
に置き換えられた時点で次の様になります。
アドレス | 関数 | 変数名 | 変数の内容 |
---|---|---|---|
1 | ByRefMain | ws | Nothing |
2 | ByRefTest | Arg | 1番への参照 |
100 | Sheet1オブジェクト |
コメント
コメント一覧 (1件)
参考書や他のサイトを読んでも「ケースによって使い分けが必要です」という解説にしか出会えずモヤモヤしていたのですが、この記事の「ほとんどのケースでは値渡しを使うべき」という一言でスッキリしました。