MENU

VBAにおける値渡しと参照渡しの違い(ByVal/ByRef)

コーディング規約を自分でまとめ直そうと思って他の人のガイドラインを見ていたら、値渡しと参照渡しに関する間違いを発見したのでそれについて。ついでに、変数の値型と参照型の違いの知識も必要になるので、それについても説明していきます。

目次

メモリーとアドレス

実行中のプログラムはメモリー上に読み込まれていますが、そのメモリーにはアドレス(番地)が割り振られています。プログラム(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オブジェクト
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメント一覧 (1件)

  • 参考書や他のサイトを読んでも「ケースによって使い分けが必要です」という解説にしか出会えずモヤモヤしていたのですが、この記事の「ほとんどのケースでは値渡しを使うべき」という一言でスッキリしました。

コメントする

目次