和風スパゲティのレシピ

日本語でコーディングするExcelVBA

ByVal(値渡し)とByRef(参照渡し)の違い

関数に引数を渡す際の渡し方である、
ByVal(値渡し)とByRef(参照渡し)の違いを説明します。

同じところ

どちらも関数に値(引数)を渡す渡し方です。

コード上では、

Sub 関数の名前(By○○ 引数の名前 As データ型)

この○○に入るキーワードで使い分けます。

違うところ

ざっくり説明すると、関数に値(引数)を渡すときに、

  • 「変数に書いてある情報だけ」を渡すのが値渡し(ByVal)
  • 「変数をそのまま」渡すのが参照渡し(ByRal)

です。


例えばマクロさんが「1000円でみかん買ってきて!」と関数さんに頼むとしましょう。

この「1000円で」という部分は、意味の取り方が2つありますよね?

  1. 「1000円以内で」という金額の情報(払うのは関数さん)
  2. 「1000円渡すから」という紙幣そのもの(払うのはマクロさん)

このどちらの意味でも通じます。

1が夫婦、2が上司部下みたいな。


この1000円の意味の取り方の内、1を値渡し2を参照渡しと呼びます。


この風景を、実際にVBAでコーディングしてみましょう。

Sub マクロさんが関数さんにみかんの買い出しを頼む

    Dim お金 As Long
    お金 = 1000

    Call みかんを買えるだけ買う(お金)

    MsgBox(お金) ' ← 下の関数の「○○」の違いで、ここの結果が変わるよ!

End Sub

Sub みかんを買えるだけ買う(By○○ 資金 As Long)
    みかんの個数 = みかんの個数 + 資金 \ 150
    資金 = 資金 Mod 150
End Sub

みかんの個数まで表現するとわかりづらくなるので、
なんかのPublic変数だと思って無視してください。


この関数宣言部の○○部分を、

Sub みかんを買えるだけ買う(ByVal 資金 As Long)

と「ByValで」書いた場合は、「値渡しの関数 ⇒ 情報を渡すだけ」なので、
 

Call みかんを買えるだけ買う(お金)

ここで渡す「お金」は、関数さんに金額を伝えただけです。


よって「MsgBox(お金)」は、宣言時と変わらず1000を返します。



対して、

Sub みかんを買えるだけ買う(ByRef 資金 As Long)

と「ByRefで」書いた場合は、「参照渡しの関数 ⇒ 変数をそのまま渡す」ので、
 

Call みかんを買えるだけ買う(お金)

ここで渡す「お金」は、関数さんに千円札を渡しています


よって「MsgBox(お金)」は、150円を6つ買って残った100を返します


これがByVal(値渡し)とByRef(参照渡し)の違いとなります。

使い分け

さてこの使い分けですが、これが結構難しいです。

関数の目的でどっちを使うべきかが決まります


ひとまず↑の例では、わかりやすいように「関数さんが払う」としていましたが、
実際のコードでは、関数さんの持ち物なんてありませんので、

Sub みかんを買えるだけ買う(ByVal 資金 As Long)
    みかんの個数 = みかんの個数 + 資金 \ 150
    資金 = 資金 Mod 150 ' ← 実は全く無意味なコード
End Sub

この2行目のコードには全く意味はないですよね?

値渡し = ただの情報 なので、End Subと同時に破棄される「資金」を、
最後に計算している意味はありません。


ということで、今回の例を「コード上で意味があるように」しっかり書きなおすと、以下の通りとなります。


◇ 参照渡し(ByRef)バージョン

Sub みかんを買い足す

    Dim 所持金 As Long
    所持金 = 1000

    Dim みかんの個数 As Long
    みかんの個数 = 10

    Call みかんを買えるだけ買う(所持金, みかんの個数)

    MsgBox("残金:" & 所持金 & " 残みかん:" & みかんの個数)

End Sub

Sub みかんを買えるだけ買う(ByRef 資金 As Long, ByRef みかんの個数 As Long)
    みかんの個数 = みかんの個数 + 資金 \ 150
    資金 = 資金 Mod 150
End Sub

 
◇ 値渡し(ByVal)バージョン

Sub みかんを買い足す

    Dim 所持金 As Long
    所持金 = 1000

    Dim みかんの個数 As Long
    みかんの個数 = 10

    Dim みかんの購入数 As Long
    みかんの購入数 = 買えるみかんの最大数(所持金)

    所持金 = 所持金 - 150 * みかんの購入数
    みかんの個数 = みかんの個数 + みかんの購入数

    MsgBox("残金:" & 所持金 & " 残みかん:" & みかんの個数)

End Sub

Function 買えるみかんの最大数(ByVal 資金 As Long) As Long
    買えるみかんの最大数 = 資金 \ 150
End Function

こんな感じになります。

この二つのマクロでやっていることは同じなので、
最後のMsgBoxはどちらも「残金:100 残みかん:16」と表示されます。


参照渡しバージョンでは、購入するという「処理を関数に」切り分けています。

処理をやってもらいますので、所持金、みかんの個数共にByRefで渡し、
この変数の書き換えをやってもらうことになります。


対して値渡しバージョンでは、何個買えるかという「計算を関数に」切り分けています。

計算をするだけですので、所持金はByValで渡せばよいですし、みかんの現在の個数は渡す必要はありません。

その代わり、メインマクロの中で購入する処理は行う必要があるということです。


使い分けの説明はこんな感じです。

ByVal(値渡し)とByRef(参照渡し)に使い分けがあるというよりは、
関数化の目的があり、それによってどっちを使うべきか決まるという話ですね。


ByVal(値渡し)とByRef(参照渡し)の使い分けを理解するということは、
プロシージャ分割の原理をきっちり学ぶことに等しいと思います。

いきなり完璧に理解する必要はないと思いますので、
関数をいっぱい切り分けながら、ちょっとずつ慣れていきましょう。

キーワードの書き分け(省略の是非)について

ByVal(値渡し)とByRef(参照渡し)を使い分ける際、
どちらも書かない場合は「ByRef」になります


このことについて、

  • ByRefって省略していいの?
  • 書き分けるルールはどうすればいいの?

あたりもよく質問されることなので、あわせて解説していきます。


さて、使い分けで説明した「値渡し」バージョンの

Function 買えるみかんの最大数(ByVal 資金 As Long) As Long
    買えるみかんの最大数 = 資金 \ 150
End Function

これですが、ぶっちゃけこれってByRefでも問題ないですよね?


中で「資金」という変数を書き替えていないので、
別にByRefで渡しても、呼び出し元のお金を使うことはありません。

「1000円と言わずに1000円札を渡した」ことになりますが、
そのお札を見ながら電卓をたたいても問題はないわけです。


ということで、
処理(書き換え)をやってもらう関数は必ずByRefにする必要があります」が、
計算するだけの関数は必ずしもByValにする必要はありません」。


このことと、キーワードを省略時に「ByRef」になることを踏まえて、
書き換える予定のない引数にByValをきっちりつけるかどうかは好みです。


「値として使う場合は必ずByValにする派」もたくさんいらっしゃいます。

メリットは

  • 間違って書き換えてしまってもメインマクロに影響を与えない
  • そもそも値として使うなら、ちゃんと値だと書くべき

デメリットは

  • ほぼ全部の引数に「ByVal」を書く羽目になって面倒で邪魔
  • ByByByByうるさくて、本当に重要な「ByRef」という記述を見逃しやすくなる

あたりでしょうか。


対して「引数を関数内で書き換えるつもりの時だけByValを書き、どっちでもいい場合は省略する派」もいます。

メリットは

  • 引数宣言部が短くて見やすい
  • ByValと明示することが「書き換えますよ宣言」とみなせる

デメリットは

  • 書き換えが必要になった場合に変え忘れるとバグる

です。


私は後者で書くことの方が多いですかね。

Function 関数(ByVal A, ByVal B, ByVal C, ByRef D, ByVal E)

Function 関数(A, B, C, ByRef D, ByVal E)

この2つを比べたときに、
「Aをあとあと書き換えることになったときの危険性」
よりも、
「Dはいじって返す、Eはいじるけど返さない、ABCはいじらないのでどうでもいい」
と読み手に伝えることができるメリットを取っているイメージです。


ただし、いずれの派閥も「書き換えて返すつもりのD」のByRefは省略していないと思います。


「参照渡しで渡した引数を書き換える」というのは重要な情報ですので、
省略時はByRefだからと、このByRefを書かないのはおすすめしません。


ちなみにこの考察を見てわかる通り、この仕様はMicrosoftさんに「省略時はByValにしとけや」って言いたくなるような仕様です(笑)

「VBAのここが嫌」ランキングで、よく上位に挙げられている気がします(´∀`;)

おまけ:暗黙の型変換のためのByVal

おまけといいつつ、実は一番実務に活かせる情報かもしれません(笑)

Function 買えるみかんの最大数(資金 As Long) As Long
    買えるみかんの最大数 = 資金 \ 150
End Function

このようにBy○○を省略して作った関数は「ByRef」となっていますが、
この関数をこう呼ぶとエラーとなります↓
 

Dim 指示内容 As String
指示内容 = "1000円で買って来て!"

指示内容 = Left(指示内容, 4)
Debug.Print 買えるみかんの最大数(指示内容)

「ByRef 引数の型が一致しません」と怒られます。

Stringの変数で受け取った指示内容から、金額部分を抽出していますが、
設定した引数がLongで渡す変数がStringだとエラーになるのです。


この対策に「ByVal」を使うことができ、

Function 買えるみかんの最大数(ByVal 資金 As Long) As Long
    買えるみかんの最大数 = 資金 \ 150
End Function

とすることで、"1000"が1000に暗黙の型変換されるようになり、エラーが出なくなります。

この仕様も覚えておきましょう。


なお、この「型の不一致のエラー」は、変数をそのまま渡した場合にのみ出ます。

Debug.Print 買えるみかんの最大数(Left("1000円で買って来て", 4))

と、直接Leftで取得した場合はByRefでも動きます。

値を渡しているため、参照渡しの設定でも値渡しで渡るためです。


Stringの変数をLongの引数に渡す場合は、

Debug.Print 買えるみかんの最大数(指示内容 * 1)

と、1を掛けて変数でなくしてから渡せばByRefでも動きます。


この裏技?も、覚えておくと何かの役に立つかもしれません。