検証環境: 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 は名前では何も返しませんが、呼ぶ側の low と high に書き込んで二つの数を返します。これが 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 が守るのは 単純な 値——数値・文字列・ブール・日付——です。オブジェクトを深くコピーは しません。Range・Worksheet・Workbook では、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 の Sub とは — プロシージャの呼び出し方と「マクロの正体」
- VBA の Function とは — 戻り値とユーザー定義関数(UDF)
- VBA MsgBox の使い方 — はい/いいえ・ボタン・括弧ルール
- VBA For ループ — 実務で使う 8 つの例
よくある質問
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 を無効化します。見た目は似ていても逆に動きます。
