こんにちは!いつもExcel VBAの開発、お疲れ様です。
マクロの記録を卒業し、自分で「プロシージャ(SubやFunction)」を分割して書けるようになってくると、VBAの実力がグッと上がった実感が湧いてきますよね。
しかし、自分でコードを組み立てるようになると、多くの人が「なぜか、元の変数の値が勝手に変わってしまう…」という奇妙なバグに遭遇します。
「何もしていないのに、計算結果がおかしい」
「呼び出したプログラムの先で、変数の値が書き換わっている気がする」
実はこれ、VBAの仕様である「ByVal(値渡し)」と「ByRef(参照渡し)」の理解不足が引き起こす罠なのです。
ここをクリアすれば、Excel VBAの基本はバッチリですよ!
今回は、あなたのコードをバグから守り、劇的に安全性を高める「防御的プログラミング」の考え方を、先輩エンジニアが優しく、かつ本質まで徹底的に解説します。
—
1. ByVal と ByRef の違いをイメージで理解しよう
まずは、難しい専門用語を脇に置いて、イメージしやすい「例え話」から始めましょう。
プロシージャに変数を渡す方法は、大きく分けて2つあります。
| 渡し方 | イメージ | 特徴 |
| :— | :— | :— |
| ByVal (値渡し) | 「書類のコピー」を渡す | 相手がコピーに何を書き込んでも、手元にある原本は汚れない。 |
| ByRef (参照渡し) | 「原本の保管場所(住所)」を教える | 相手がその場所に行って書類を書き換えると、原本そのものが変わる。 |
【ByVal(値渡し)のイメージ】
[呼び出し元: 変数A(10)] ──(コピーを渡す)──> [呼び出し先: 引数(10)]
│ (中で値を50に変える)
[呼び出し元: 変数A(10)] <──(原本は10のまま)───── [呼び出し先: 引数(50)]
【ByRef(参照渡し)のイメージ】
[呼び出し元: 変数A(10)] ──(保管場所を教える)─> [呼び出し先: 変数Aを直接操作]
│ (中で値を50に変える)
[呼び出し元: 変数A(50)] <──(原本が書き換わる)─── [呼び出し先: 変数A(50)]
VBAでは、引数の前に `ByVal` または `ByRef` を書くことで、どちらの方法で渡すかを指定します。
そして、ここが最重要の注意点です。
VBAは、何も書かないと自動的に「ByRef(参照渡し)」になります。
この「デフォルトが参照渡し」という仕様こそが、多くの「意図しないバグ」を生み出す原因になっているのです。
—
2. 【実験】「意図しない変更」が発生する瞬間を体感しよう
言葉だけではピンとこないかもしれませんね。実際に簡単なコードを動かして、その違いを体感してみましょう!
以下のコードを、ExcelのVBAエディタ(VBE)の標準モジュールに貼り付けて実行してみてください。
パターンA:危険な「ByRef(参照渡し)」の例
まずは、何も考えずに引数を渡した場合の挙動です。
‘ ————————————————————
‘ メイン処理(実行するマクロ)
‘ ————————————————————
Sub Main_ByRef_Example()
Dim price As Long
price = 1000 ‘ 元の価格は1000円
MsgBox “【呼び出し前】商品の価格は ” & price & ” 円です。”
‘ 割引計算プロシージャを呼び出す(引数をそのまま渡す=ByRefになる)
Call CalculateDiscount_ByRef(price)
‘ ★ここに注目!
MsgBox “【呼び出し後】元の変数の値は ” & price & ” 円になってしまいました!”
End Sub
‘ ————————————————————
‘ 割引率を計算して表示するサブ処理(ByRef)
‘ ————————————————————
Sub CalculateDiscount_ByRef(ByRef targetPrice As Long)
‘ 20%引きにする計算のつもり
targetPrice = targetPrice 0.8
MsgBox “【処理中】割引後の価格は ” & targetPrice & ” 円です。”
End Sub
何が起きたのか?
1. `price` に `1000` を代入してスタートします。
2. `CalculateDiscount_ByRef` に `price` を渡します。
3. 呼び出された先で `targetPrice = targetPrice 0.8`(800)に書き換えます。
4. メイン処理に戻ってくると、なんと元の `price` の中身まで `800` に書き換わってしまっています!
これは、呼び出し先に「変数 `price` のメモリ上の住所」を教えてしまったため、呼び出し先が直接中身を書き換えてしまったからです。
—
パターンB:安全な「ByVal(値渡し)」の例
では次に、`ByVal` を使って「コピー」を渡すように書き換えてみましょう。
‘ ————————————————————
‘ メイン処理(実行するマクロ)
‘ ————————————————————
Sub Main_ByVal_Example()
Dim price As Long
price = 1000 ‘ 元の価格は1000円
MsgBox “【呼び出し前】商品の価格は ” & price & ” 円です。”
‘ 割引計算プロシージャを呼び出す(値渡し:ByVal)
Call CalculateDiscount_ByVal(price)
‘ ★ここに注目!
MsgBox “【呼び出し後】元の変数の値は ” & price & ” 円のまま安全です!”
End Sub
‘ ————————————————————
‘ 割引率を計算して表示するサブ処理(ByVal)
‘ ————————————————————
Sub CalculateDiscount_ByVal(ByVal targetPrice As Long)
‘ コピーされた値を計算に使用する
targetPrice = targetPrice 0.8
MsgBox “【処理中】割引後の価格は ” & targetPrice & ” 円です。”
End Sub
結果はどうなったか?
呼び出し先で `targetPrice` をいくら変更しても、メイン処理の `price` は `1000` のまま守られています。
これが「防御的プログラミング(Defensive Programming)」の第一歩です。
自分の知らないところで変数が勝手に書き換わるのを防ぐことで、予測可能で、デバッグのしやすい安全なプログラムを作ることができます。
—
3. なぜVBAは、デフォルトが「ByRef」なのか?
ここで少し、システム設計の歴史に裏付けられた面白いお話をしましょう。
「そんなに危険なら、最初からデフォルトをByVal(値渡し)にしておけばよかったのでは?」と思いますよね。実は、それには歴史的な理由があります。
VBAが誕生した10年以上前、コンピュータのメモリ(RAM)は今よりもはるかに貧弱で、動作速度も遅いものでした。
巨大なデータ(例えば、大量の文字が入った変数や、複雑な配列など)を `ByVal` で渡すと、渡すたびにメモリ上にその丸ごとコピーを作成するため、メモリを大量に消費し、動作が著しく重くなってしまったのです。
一方、`ByRef` であれば「メモリ上のここにあるよ」という、わずか数バイトの「住所(ポインタ)」を伝えるだけで済むため、どんなに大きなデータでも一瞬で、かつメモリを消費せずに渡すことができました。
つまり、「安全さ」よりも「軽さと速さ」を最優先した結果が、デフォルトのByRefだったのです。
現代の設計思想はどうあるべきか?
現代のPCは、当時とは比較にならないほど大容量のメモリを積んでいます。
そのため、通常の数値(Long)や文字列(String)を数個コピーしたところで、実行速度やメモリに影響が出ることはまずありません。
現代のソフトウェア開発においては、「動作のわずかな高速化」よりも「バグが混入しない安全な設計」の方がはるかに価値が高いとされています。
ですから、私たちは意識して `ByVal` を選ぶべきなのです。
—
4. 防御的プログラミングを実践する「3つの黄金ルール」
それでは、明日からの実務でそのまま使える、プロの設計ルールをお伝えします。
ルール①:基本はすべて `ByVal` と明記する
新しくプロシージャ(Sub / Function)を作るときは、引数の前に必ず `ByVal` と書く癖をつけてください。
‘ 悪い例(何も書かないとByRefになり、予期せぬバグの温床に)
Sub ProcessData(targetDate As Date, userName As String)
‘ 良い例(明示的にByValをつけ、元のデータを保護する)
Sub ProcessData(ByVal targetDate As Date, ByVal userName As String)
ルール②:元の値を変更したい時だけ、意図的に `ByRef` を使う
元の変数を書き換える必要がある場合のみ、「私は今、意図的に参照渡しを使っている」という強い意志を持って `ByRef` を書きます。
例えば、「処理が成功したかどうかを `Boolean` で返しつつ、エラーメッセージを元の変数に格納したい」というようなケースです。
‘ 呼び出し元のエラーメッセージ変数を直接書き換えるため、意図的に ByRef を使用
Function TryRegisterUser(ByVal userId As String, ByRef outErrorMessage As String) As Boolean
If userId = “” Then
outErrorMessage = “ユーザーIDが空欄です。” ‘ 呼び出し元の変数に直接エラーを書き込む
TryRegisterUser = False
Exit Function
End If
‘ 登録処理…
TryRegisterUser = True
End Function
ルール③:オブジェクト型を渡すときの挙動を知っておく
`Worksheet` や `Range` などの「オブジェクト型」を渡すときは少し注意が必要です。
オブジェクト型変数は、それ自体がすでに「シートやセルへの参照(リモコンのようなもの)」を指しています。そのため、`ByVal` で渡しても、「リモコンのコピー」が渡されるだけなので、そのリモコンを使ってセルの中身(`Value`)を書き換えると、当然元のセルも書き換わります。
‘ ByValで受け取っても、セル(Range)のプロパティは書き換わります!
Sub FormatCell(ByVal targetRange As Range)
‘ リモコンのコピーを使って、セルの背景色を黄色にする
targetRange.Interior.Color = vbYellow
End Sub
「オブジェクト型のプロパティ操作は、ByValであっても元のオブジェクトに影響を与える」という点だけ、頭の片隅に置いておいてくださいね。
—
まとめ:ワンランク上のVBA開発者へ
今回のポイントを振り返ってみましょう。
1. ByVal(値渡し)は「コピー」を渡すため、呼び出し元の変数が守られて安全。
2. ByRef(参照渡し)は「原本の場所」を教えるため、呼び出し先での変更が元にも影響する。
3. VBAは何も書かないとByRefになるため、予期せぬバグが起きやすい。
4. 現代のVBA開発では、「基本はすべて ByVal」と明記するのがプロのスタンダード。
プロシージャの定義に `ByVal` を追加する。たったこれだけの工夫で、あなたの作るマクロの安定性は劇的に向上します。
「ここをクリアすれば、Excel VBAの基本はバッチリですよ」
焦らず、一つひとつのコードに「意図」を込めて書いてみてください。あなたのVBAライフが、より楽しく、そして安全なものになることを応援しています!
