🚀The world's best VBA AI has evolved. ExcelMaster is now an autonomous Agent.Read more →
Back to Blog

VBA の ByRef vs ByVal — 呼び出し後に変数が変わる理由(Excel)

|

VBA の ByRef vs ByVal — 呼び出し後に変数が変わる理由(Excel)

検証環境: Excel 365 v2509 · Excel 2021 · Excel 2019 · 最終確認 2026-06-07

要点ByRef(参照渡し)は 元の変数 を渡すので、プロシージャがそれを書き換えられます。ByVal(値渡し)は コピー を渡すので、元は安全です。VBA は既定で ByRef——だから呼び出しのあとに変数が知らぬ間に変わるのです。

Sub Demo()
    Dim n As Long: n = 10
    AddOne n
    MsgBox n            ' 11 ——n は本当に変わった。既定が ByRef だから
End Sub

Sub AddOne(ByRef x As Long)   ' ByRef:x は n そのもの ——x への書き込みは n への書き込み
    x = x + 1
End Sub
' 上の「ByRef」を「ByVal」に変えると MsgBox は 10 ——元は手つかず。

この既定——何も指定しなければ ByRef——こそ、VBA の「なぜ変数が変わった?」系バグのほぼすべての元です。もう二度と驚かされない方法を説明します。

メンタルモデル:元のノートか、コピーか

プロシージャに変数を渡すとは、データへのアクセスを渡すこと。渡し方は二通りあります。

  • ByRef = 本物のノートを手渡す。 プロシージャは実際のページに書き込みます。ノートが返ってくると、あなたの変更がそこにあります。(Ref = 参照 = 「元への指し示し」。)
  • ByVal = 1 ページのコピーを手渡す。 プロシージャはコピーにいくら書き込んでも構いません。あなたのノートは変わりません。(Val = = 「中身のスナップショット」。)

同じ発想で、結果が二つ。ByRef ならプロシージャ内の変更は 残りByVal ならプロシージャが終わった瞬間に 消えます。あとはすべて細部です。

たった一つのルール:既定は ByRef、そこが罠

噛みついてくる事実がこれです。ByVal と書かなければ、VBA は ByRef を使います。 次の 2 つの宣言は同じです。

Sub Process(x As Long)        ' キーワードなし...
Sub Process(ByRef x As Long)  ' ...これとまったく同じ意味

つまり、あなたが書いた印のない引数は すべて 黙って参照渡しです。たいていは問題になりませんが、渡したカウンターを補助プロシージャがこっそり増やした日、ありえないように見えるバグになります——変数が変わり、変えた行は 別の プロシージャにある。Python・Java・C#(プリミティブは値渡し)から来た人には逆さまに感じられます。そのとおり、ここでは VBA が例外なのです。

この種のバグを丸ごと防ぐ習慣:すべての引数に明示的に ByVal と書き、「値を返したい」と本気で思うときだけ ByRef に切り替える。 夕方 6 時にデバッグする未来のあなたが感謝します。

' 既定で安全 ——呼ぶ側の変数を守る:
Sub LogRow(ByVal rowNum As Long, ByVal label As String)
    Debug.Print rowNum & ": " & label
End Sub

意図して ByRef を使う:答えが二つ以上ほしいとき

ByRef は避けるべき間違いではなく、道具です。Function はちょうど一つの値を返します。プロシージャに 二つ以上 の結果を返させたいときは、ByRef の出力引数がきれいで古典的な方法です。

Sub MinMax(data As Range, ByRef lo As Double, ByRef hi As Double)
    lo = Application.Min(data)
    hi = Application.Max(data)
End Sub

Sub UseIt()
    Dim low As Double, high As Double
    MinMax Range("A1:A100"), low, high   ' 両方の変数を埋める
    MsgBox "範囲:" & low & " ~ " & high
End Sub

MinMax は名前では何も返しませんが、呼ぶ側の lowhigh に書き込んで二つの数を返します。これが ByRef の設計どおりの仕事です。目安は——入力は ByVal、出力は ByRef 意図して印を付ければ、シグネチャ自体が説明になります。

ByRef を ByVal に裏返す罠:余分な括弧

これは本当に厄介で、Sub の呼び出し方に直結します。引数を 余分な 括弧で囲むと、まず式として評価されます——つまり VBA は 結果、すなわちコピーを渡し、ByRef を無効化します。

AddOne n        ' 意図どおり ByRef → n は 11 になる
AddOne (n)      ' (n) が先に評価される → コピーが渡る → n は 10 のまま
Call AddOne(n)  ' ここの括弧は Call のもの → 再び ByRef → n は 11 になる

AddOne (n)Call AddOne(n) はほとんど同じに見えて、 に動きます。ByRef の引数が呼ぶ側の変数を更新しないと不思議に思ったら、引数を囲む迷子の括弧を探してください。これは意図的な小技でもあります——プロシージャが ByRef でも変数を守りたいとき、( ) で囲むのが手早いローカルな上書きです。

誰もが誤解する細部:オブジェクトは違いを無視する

ByVal が守るのは 単純な 値——数値・文字列・ブール・日付——です。オブジェクトを深くコピーは しませんRangeWorksheetWorkbook では、ByVal でも 参照 のコピーを渡し、そのコピーは依然として 同じ生きたオブジェクト を指します。

Sub Clear(ByVal rng As Range)   ' ByVal なのに...
    rng.ClearContents           ' ...呼ぶ側のセルを本当にクリアする
End Sub

ByVal が Range を「守る」と思って使い、セルがそれでも消えて困惑する人がいます。真実は——オブジェクトへの ByVal は、プロシージャ内で変数を 別の オブジェクトに付け替えるのを止めるだけで、オブジェクトの中身は決して守りません。 中身を無傷にしたいなら、値を自分でコピーして取り出してください。キーワードはやってくれません。

よくある ByRef / ByVal の誤り(と直し方)

症状 原因 直し方
呼び出し後に変数が変わった 既定の ByRef がプロシージャに書き換えを許した 引数を ByVal で宣言、または中で変更しない
ByRef 引数が呼ぶ側を更新しない 引数を余分な括弧で囲んだ:MySub (x) 括弧を外す(または Call MySub(x)
「ByRef 引数の型が一致しません」 呼ぶ側の変数の型 ≠ 宣言した引数の型 型を合わせる、または ByVal でコピーを変換させる
ByVal の Range が変わった オブジェクトは ByVal でも参照を渡す 呼び出し前に値をコピー。キーワードは中身を守らない
戻り値が二つ要るのにグローバルを使った Function は一つしか返さない ByRef の出力引数 ——本来の道具
再帰でメモリ不足 大きな値を ByVal で渡し各階層でコピー 大きな配列は ByRef で渡し、呼び出しごとのコピーを避ける

データを引き回すのが手間なら —— 結果を言葉で伝える

ByRef/ByVal は、VBA を地雷原に感じさせるたぐいの細部です——正しいけれど面倒で、迷子の括弧一つでバグになる。本当の目的が「このシートを地域ごとに分け、各担当者に自分の分を送る」なら、引数のコピーのされ方を考えているべきではありません。ExcelMaster Agent は、その目的を自然な日本語で受け取って結果を出します——引数も、渡し方の約束事も、落とし穴もありません。無料で試す →

関連記事

よくある質問

VBA の ByRef と ByVal の違いは? ByRef は元の変数への参照を渡すので、プロシージャ内での変更は戻ったあとも残ります。ByVal はコピーを渡すので、元は決して変わりません。このキーワードが、プロシージャに呼ぶ側の変数を変更させるかどうかを決めます。

VBA は既定で ByRef ですか、ByVal ですか? 既定は ByRef です。Sub Process(x As Long) のようにキーワードなしで引数を書くと、VBA は ByRef として扱い、プロシージャは渡した変数を変えられます。多くの現代の言語とは逆で、よくあるバグの元です。

Sub を呼んだら変数が変わったのはなぜ? 引数が ByRef(既定)で、プロシージャがそれを変更したからです。変数を守るには、引数を ByVal で宣言してコピーを渡します。あるいは呼び出し側で引数を余分な括弧で囲み——MySub (x)——コピーを強制します。

VBA で ByRef を使うのはどんなとき? プロシージャが呼ぶ側に値を 返す 必要があるとき——たとえば出力引数で二つ以上の結果を返すとき——に意図して ByRef を使います。目安は「入力は ByVal、出力は ByRef」。

ByVal は Range や Worksheet オブジェクトを守りますか? いいえ。ByVal は参照だけをコピーし、オブジェクトはコピーしません。コピーは同じ生きた Range や Worksheet を指したままなので、中身を変えれば呼ぶ側にも影響します。オブジェクトへの ByVal は、プロシージャ内で変数を別のオブジェクトに付け替えるのを防ぐだけです。

Call MySub(x)MySub (x) と何が違いますか? Call では括弧は Call 構文の一部なので、x は依然 ByRef で渡ります。Call なしの (x) は先に評価される式なので、コピー が渡り——ByRef を無効化します。見た目は似ていても逆に動きます。