MENU

Excel VBA 参照渡し「ByRef」と値渡し「ByVal」

Excel VBA 参照渡し「ByRef」と値渡し「ByVal」
目次

はじめに

前回のFunctionプロシージャの解説で、引数の説明の中で「参照渡し」と「値渡し」のサンプルを使いましたが「参照渡し」と「値渡し」ってわかりますか?

ByRef(参照渡し)は変数そのものを渡し、ByVal(値渡し)は値のコピーを渡します。 そのため、呼ばれた側で引数を変更すると、ByRefは呼び出し元の変数も変わり、ByValは変わりません。

引数にByRefなんて書いたことがないんですが、関係ありますか?(^^;

くるみこ

書いていなくても、実はByRefとして動いているんです。実際に動かしながら確認しましょう(^^)/

VBAでは指定を省略するとByRefになるので、気づかないうちに呼び出し元の変数を書き換えてしまうことがあります。迷ったときは、基本はByValを使い、呼び出し元の値を変更したいときだけByRefを使うと、この種の不具合を防げます。

【この記事でわかることは】次の内容を実行結果付きで解説します。

  • ByRefとByValの動作の違い
  • 指定を省略したときに起こりやすい不具合
  • 使い分けの基準
  • 配列やオブジェクトを渡すときの注意点
  • 「ByRef 引数の型が一致しません」の対処法

前回記事のおさらいは、下のカードをクリックすれば開きます(^^ゞ

前回記事は、Functionプロシージャの使い方に関する解説記事でした。覗いてみてね(^^)/

ByRefとByValの違い

まず、違いを表で確認します。

項目ByRef(参照渡し)ByVal(値渡し)
渡すもの変数そのもの値のコピー
呼ばれた側で変更すると呼び出し元の変数も変わる呼び出し元の変数は変わらない
指定を省略したときこちらになる明示が必要
主な用途呼び出し元の値を変更したいとき値を受け取って使うだけのとき
ByRefは呼び出し元と同じ変数を使い、ByValはコピーした別の変数を使うことを示す図

動作を確認するサンプル

「参照渡し」「値渡し」という言葉だけで考えると、渡す側に違いがあるように感じてしまいます。しかし、通常は引数を渡す側を見ても判別できません。でも、引数を受け取る側のプロシージャの定義(引数宣言)を見れば判別できます。

では、以下の簡単なサンプルを使って確認してみましょう。

'参照渡しと値渡しの確認
Sub ByRefValSample()
    Dim a As String
    a = "くるみこ"  '変数に文字列"くるみこ"を入れる

    'プロシージャByRefSampleの引数に変数を渡して呼び出す
    Call ByRefSample(a)
    MsgBox a & " 参照渡し(ByRef)"   '変数の値を表示する

    'プロシージャByValSampleの引数に変数を渡して呼び出す
    Call ByValSample(a)
    MsgBox a & " 値渡し(ByVal)"     '変数の値を表示する
End Sub

'参照渡し
Sub ByRefSample(ByRef a As String)
    a = a & "ちゃんはかわいい!"   '変数に文字列を追加
End Sub

'値渡し
Sub ByValSample(ByVal a As String)
    a = "kurumicoだよ!"           '変数に文字列を代入
End Sub

参照渡しの結果は、次のとおりです。

「くるみこちゃんはかわいい!参照渡し(byRef)」を表示したMsgBox画像

変数を渡したプロシージャで変数に文字列”ちゃんはかわいい!”を追加すると、呼び出し元の変数も変更されました。

値渡しの結果は、次のとおりです。

「くるみこちゃんはかわいい!値渡し(byVal)」を表示したMsgBox画像

値渡しでも変数に「kurumicoだよ!」を代入していますが、呼び出し元の変数には反映されていないことが確認できました。

指定を省略するとByRefになる

ByRef も ByVal も書かなかった引数は、ByRefとして扱われます。次の税込金額を求めるコードは、この動作が原因で2回目の結果がおかしくなります。

Sub TaxBug()
    Dim price As Long
    price = 1000

    Debug.Print "1回目: " & AddTax(price)   ' 1回目: 1100
    Debug.Print "2回目: " & AddTax(price)   ' 2回目: 1210
End Sub

Function AddTax(amount As Long) As Long
    amount = amount * 1.1
    AddTax = amount
End Function

同じ1,000円を渡しているつもりでも、1回目の処理で price が1,100に変わるので、2回目は1,100円に税が加算されます。エラーは出ないため、結果を見るまで気づきにくい不具合です。

引数を ByVal にすると、呼び出し元の price は変わらなくなります。

Function AddTax(ByVal amount As Long) As Long
    amount = amount * 1.1
    AddTax = amount
End Function

エラーが出ないのに結果だけ変わるなんて、気づくのが大変そうです(^^;

くるみこ

そうなんです。引数を書き換える処理では、ByRefとByValのどちらなのかを意識しましょう(^^)

ByRefとByValの使い分け

判断の基準は、呼び出し元の変数を変更したいかどうかです。

やりたいこと使うもの
値を受け取って使うだけByVal
結果を1つ返すFunctionの戻り値
結果を2つ以上返すByRef
配列を渡すByRef(後述)

ByValでは値をコピーしますが、数値や短い文字列であれば、コピーの負担を気にする必要はほとんどありません。処理速度より、意図しない書き換えを防ぐことを優先するのがおすすめです。

Functionの戻り値の書き方は、Excel VBA Functionプロシージャを使ってみようで解説しています。

ByRefで複数の値を返す

Functionの戻り値は基本的に1つなので、複数の結果を返したいときはByRefが便利です。次のサンプルは、割り算の商と余りを同時に受け取ります。

Sub DivideSample()
    Dim q As Long
    Dim r As Long

    Divide 17, 5, q, r
    Debug.Print "商: " & q & " 余り: " & r   ' 商: 3 余り: 2
End Sub

Sub Divide(ByVal a As Long, ByVal b As Long, _
           ByRef quotient As Long, ByRef remainder As Long)
    quotient = a \ b
    remainder = a Mod b
End Sub

入力用の引数をByVal、結果を返す引数をByRefと書き分けると、どの引数が変更されるのかがコードから読み取れます。

配列を渡すときはByRefで受け取る

配列の変数を () 付きの引数で受け取るときは、ByRefしか指定できません。ByVal values() As Long と書くと、コンパイルエラーになります。

Sub ArraySample()
    Dim nums(1 To 3) As Long
    nums(1) = 1
    nums(2) = 2
    nums(3) = 3

    DoubleAll nums
    Debug.Print nums(1) & ", " & nums(2) & ", " & nums(3)   ' 2, 4, 6
End Sub

Sub DoubleAll(ByRef values() As Long)
    Dim i As Long
    For i = LBound(values) To UBound(values)
        values(i) = values(i) * 2
    Next i
End Sub

元の配列を変更したくない場合は、ByVal のVariant型で受け取ります。配列全体がコピーされるので、大きな配列では処理時間とメモリが増える点に注意してください。

Sub ArrayCopySample()
    Dim nums(1 To 3) As Long
    nums(1) = 1

    ChangeCopy nums
    Debug.Print nums(1)   ' 1(元の配列は変わらない)
End Sub

Sub ChangeCopy(ByVal values As Variant)
    values(1) = 100
End Sub

配列の基本は、Excel VBA 配列とは?基礎的使い方をサンプルで詳しく解説をご覧ください。

オブジェクトをByValで渡すとどうなるか

RangeやCollectionなどのオブジェクトをByValで渡しても、オブジェクトの中身は呼び出し元と共有されたままです。コピーされるのは、オブジェクトそのものではなく、オブジェクトを指す参照だからです。

オブジェクト変数をByValで渡すと参照だけがコピーされ、同じオブジェクトを指すことを示す図
Sub ObjectSample()
    Dim items As Collection
    Set items = New Collection

    AddItem items
    Debug.Print "追加後: " & items.Count        ' 追加後: 1

    ReplaceByVal items
    Debug.Print "ByValで差し替え: " & items.Count   ' ByValで差し替え: 1

    ReplaceByRef items
    Debug.Print "ByRefで差し替え: " & items.Count   ' ByRefで差し替え: 0
End Sub

Sub AddItem(ByVal c As Collection)
    c.Add "A"   ' 中身の変更は呼び出し元に反映される
End Sub

Sub ReplaceByVal(ByVal c As Collection)
    Set c = New Collection   ' 差し替えは呼び出し元に反映されない
End Sub

Sub ReplaceByRef(ByRef c As Collection)
    Set c = New Collection   ' 差し替えも呼び出し元に反映される
End Sub

違いが出るのは、Set で別のオブジェクトに差し替えたときだけです。ByValでも、セルの値の変更やCollectionへの追加は、そのまま呼び出し元に反映されます。

ByValなら中身も守られると思っていました(^^;

くるみこ

くるみこ:オブジェクトでは間違えやすいところです。ByValで守られるのは変数の参照だけ、と覚えておきましょう(^^)/

呼び出し側の括弧で値渡しになる

ByRefの引数でも、呼び出すときに変数を括弧で囲むと、値のコピーが渡されます。

Sub ParenSample()
    Dim a As String
    a = "くるみこ"

    ByRefSample (a)            ' a は変わらない
    Call ByRefSample((a))      ' a は変わらない
    Debug.Print a              ' くるみこ

    ByRefSample a              ' a が変わる
    Debug.Print a              ' くるみこちゃんはかわいい!
End Sub

ByRefSample は、最初のサンプルで作成したプロシージャです。

ByRefSample (a) のように、プロシージャ名と括弧の間に空白があるコードは、括弧内が式として評価されます。式の結果は一時的な値なので、呼び出し元の a には影響しません。意図せずこの書き方になると、ByRefなのに値が戻らない原因になります。

よくある質問・エラー対処

Q. 「ByRef 引数の型が一致しません」と表示される場合は?

→ ByRefの引数と、渡した変数の型が一致していないことが原因です。特に多いのが、次のように1行で複数の変数を宣言している場合です。

Sub MismatchSample()
    Dim a, b As Long   ' a はVariant型、b はLong型になる
    a = 1
    b = 2
    ShowValue a        ' ここでコンパイルエラーになる
End Sub

Sub ShowValue(ByRef x As Long)
    Debug.Print x
End Sub

Dim a As Long, b As Long のように変数ごとに型を指定すると解消します。呼ばれた側で値を変更しないなら、引数を ByVal x As Long に変更しても解消します。

ByRef 引数の型が一致しなかった際に表示されるコンパイルエラメッセージ画像

Q. ByRefの引数に数値や計算式を直接渡せますか?

→ 渡せます。Foo 10 や Foo n + 1 は、一時的な値が渡されるだけなので、呼び出し元の変数には影響しません。

Q. 文字列や長い配列は、ByRefのほうが速いですか?

→ コピーが発生しない分、ByRefのほうが有利です。ただし、文字列を1回渡す程度なら差はほとんど感じられません。大量のループや大きな配列で問題になったときに、ByRefへの変更を検討してください。

Q. Publicのグローバル変数とByRefは、どちらを使うべきですか?

→ 引数で渡すByRefをおすすめします。グローバル変数はどこからでも変更できるので、値が変わった原因を追いにくくなります。

まとめ(おわりに)

まとめと感想など

くるみこ

「値渡し」と「参照渡し」については理解できましたか?

今まで何も書かずに、全部ByRefで使っていたことがわかりました(^^;

くるみこ

これからは、引数ごとにByRefとByValを選んでみてくださいね(^^)/

わかったこと

【今回分かったことは】

  • ByRefは変数そのものを渡し、ByValは値のコピーを渡す
  • 指定を省略するとByRefになり、意図しない書き換えの原因になる
  • 基本はByValを使い、呼び出し元の値を変更したいときだけByRefを使う
  • 配列の変数を () 付きで受け取るときはByRefのみ
  • オブジェクトはByValでも中身が共有される
  • 呼び出し側で変数を括弧で囲むと、ByRefでも値渡しになる

関連記事

今後の記事について

今回の記事はいかがだったでしょうか。皆さまのお役に立てたなら幸いです(^^;
「汎用でだれでも使えて活用できるように考えてVBAを使う」というポリシーで、記事を継続して書いていきたいと思っています。どうぞよろしくお願いしますm(_ _)m

【検討中の今後の記事内容は・・・・・】
・実務に役立つものを提供できるよう常に検討しています(^^ゞ
・その他雑記的に「プチネタなど」もいろいろ考えていきたいと思います・・・・・
・今後の記事にもご期待ください(^^)/

記事のサンプルファイルをダウンロードできます

今回の記事のサンプルはこのリンク先からダウンロードできるようにしています!
1つのブックで両方を試せるよう、ByValに修正した例は関数名を AddTaxFixed、実行用のマクロ名を TaxFixed にしています。MismatchSample はコンパイルエラーになる例なので、コメントとして収録しています。

過去の記事で使用したサンプルファイルがダウンロードできるページを設置しています
こちら(このリンク先)からご利用ください

よかったらシェアしてね!
  • URLをコピーしました!
目次