【入門編】プロシージャの引数における「ByRef」の危険性と「ByVal」による防御的プログラミング – Excel VBA解析バイブル

スポンサーリンク

皆さん、こんにちは!Excel VBAの世界へようこそ。
マクロの記録から一歩踏み出し、ご自身の意図をコードで表現しようとされている皆さんの探究心、本当に素晴らしいです。

今日は、VBAプログラミングの「次のステップ」へと皆さんを誘う、非常に大切なテーマについてお話しします。それは、プロシージャの引数における「ByRef」の危険性と、「ByVal」による防御的プログラミングです。

「え、引数って、ただ値を渡すだけじゃないの?」

そう思われた方もいるかもしれませんね。ですが、ここにはVBAならではの、ちょっとした「落とし穴」と、それを避けるための「知恵」が隠されています。ここをクリアすれば、Excel VBAの基本はバッチリですよ。さあ、一緒にこの本質を深掘りしていきましょう!

1. プロシージャと引数:コードをスマートにまとめる基礎

まずは、プロシージャと引数の基本を、改めて確認しておきましょう。

プロシージャって何だっけ?

プロシージャとは、特定の処理のまとまりに名前をつけたものです。Subプロシージャ(Subプロシージャ名 End Sub)やFunctionプロシージャ(Function関数名 End Function)がありますね。

例えば、メッセージボックスを表示するだけのシンプルなプロシージャを考えてみましょう。

‘ これは引数なしのプロシージャです
Sub SayHello()
MsgBox “こんにちは、VBAの世界へ!”
End Sub

この`SayHello`プロシージャを実行すれば、いつでも「こんにちは、VBAの世界へ!」というメッセージが表示されます。

引数って何?プロシージャに「情報」を渡す仕組み

では、このメッセージを毎回変えたい場合はどうでしょう?そんな時に使うのが「引数(ひきすう)」です。引数とは、プロシージャが実行される際に、外部から受け取る情報(データ)のこと。これにより、プロシージャの汎用性がぐっと高まります。

‘ これは引数ありのプロシージャです
Sub SayCustomMessage(messageText As String)
MsgBox messageText
End Sub

この`SayCustomMessage`プロシージャは、`messageText`という名前の引数(データ型は`String`、つまり文字列)を受け取るように定義されています。呼び出す側は、以下のように使います。

Sub TestSayCustomMessage()
Call SayCustomMessage(“今日のタスクは完了しましたか?”) ‘ 引数を渡してプロシージャを実行
Call SayCustomMessage(“お疲れ様でした!”) ‘ 別の引数を渡して実行
End Sub

このように、引数を使うことで、同じプロシージャを異なるデータで再利用できるようになります。とても便利ですよね!

2. VBAのデフォルトの振る舞い:「ByRef」の正体とその危険性

さて、ここからが本題です。VBAの引数には、実は「デフォルトの振る舞い」があり、これが思わぬバグの原因となることがあります。その振る舞いの正体こそが、ByRef(バイリフ)、つまり「参照渡し」です。

「参照渡し(ByRef)」とは?

VBAでは、引数を明示的に指定しない場合、自動的に`ByRef`として扱われます。`ByRef`は「By Reference」の略で、「参照渡し」を意味します。

参照渡しとは、引数として渡された変数の「実体そのもの」をプロシージャに渡す、ということです。もっと具体的に言うと、変数がメモリ上のどこにあるかを示す「住所(メモリアドレス)」を渡している、とイメージしてください。

ちょうど、友達に大切な書類の保管場所(住所)を教えて、そこに直接アクセスして中身を書き換えてもらうようなものです。

図解的表現:同じ箱を指している

[呼び出し元]
変数 MyValue = 100
↓ (ByRefで渡す)

[プロシージャ]
引数 paramValue

└───────────┐

│ (同じメモリ上の箱を指している)

┌───────────┘

└─ [メモリ上の箱] ───┐

│ 中身: 100

└───────────┘

プロシージャ内で`paramValue`の値を変更すると、それは呼び出し元の`MyValue`が指しているメモリ上の箱の中身を直接変更することになります。

ByRefの危険性:意図せぬ「副作用」

この「参照渡し」の特性が、実はプログラミング初学者の方が陥りやすいバグの温床となります。それは、呼び出し元の変数が、呼び出したプロシージャ内で意図せず書き換えられてしまうという「副作用(Side Effect)」が発生する可能性があるからです。

具体的なコードで見てみましょう。

Sub MainProgram_ByRefDanger()
Dim myNumber As Long
myNumber = 100 ‘ 初期値は100

Debug.Print “MainProgram実行前: myNumber = ” & myNumber ‘ 100

‘ ByRefで引数を渡すプロシージャを呼び出す(ByRefは省略されているがデフォルト)
Call ChangeNumber(myNumber)

Debug.Print “MainProgram実行後: myNumber = ” & myNumber ‘ 意図せず値が変わっている!?
End Sub

‘ 引数にByRefを明示していないが、VBAではデフォルトでByRefになる
Sub ChangeNumber(targetNumber As Long)
Debug.Print “ChangeNumber内(変更前): targetNumber = ” & targetNumber ‘ 100
targetNumber = targetNumber + 50 ‘ 引数の値を変更
Debug.Print “ChangeNumber内(変更後): targetNumber = ” & targetNumber ‘ 150
End Sub

このコードを実行すると、イミディエイトウィンドウには以下のように表示されます。

MainProgram実行前: myNumber = 100
ChangeNumber内(変更前): targetNumber = 100
ChangeNumber内(変更後): targetNumber = 150
MainProgram実行後: myNumber = 150

ご覧の通り、`MainProgram_ByRefDanger`内で定義した`myNumber`が、`ChangeNumber`プロシージャを呼び出しただけで、勝手に`100`から`150`に変わってしまいました!

この例は単純なので分かりやすいですが、もし何百行、何千行という大規模なコードの中で、複数のプロシージャが連鎖的に呼び出され、そのどこかで`ByRef`の引数が変更されてしまったらどうでしょう?

「あれ?この変数の値、いつの間に変わったんだ?!」と、デバッグに膨大な時間を費やすことになりかねません。これはもう、プログラミングの世界では「あるある」なバグの原因なんです。

3. 安全なプログラミングの味方:「ByVal」の導入

この「意図せぬ書き換え」という危険性から身を守るために、VBAプログラマーが身につけるべき習慣が、ByVal(バイバル)、つまり「値渡し」を基本とすることです。

「値渡し(ByVal)」とは?

`ByVal`は「By Value」の略で、「値渡し」を意味します。

値渡しとは、引数として渡された変数の「値のコピー」をプロシージャに渡す、ということです。呼び出し元の変数そのものではなく、その値の「複製」が渡されるため、プロシージャ内で引数の値を変更しても、呼び出し元の変数には一切影響がありません。

ちょうど、大切な書類を友達に渡すときに、原本ではなくコピーを渡すようなものです。友達がコピーにどんなに書き込みをしても、原本は安全なままですよね。

図解的表現:新しい箱を作ってコピーを入れる

[呼び出し元]
変数 MyValue = 100
↓ (ByValで渡す)

[メモリ上の箱 A] ───┐

│ 中身: 100

└───────────┘
↓ (値をコピー)

[プロシージャ]
引数 paramValue

└─ [メモリ上の箱 B] ───┐

│ 中身: 100 (コピー)

└───────────┘

プロシージャ内で`paramValue`の値を変更しても、それは「箱 B」の中身が変わるだけで、「箱 A」の中身(`MyValue`の値)はそのまま維持されます。

ByValによる防御的プログラミング

`ByVal`を使うことで、プロシージャの内部で引数の値を変更しても、呼び出し元の変数には影響しないことが保証されます。これにより、コードの予測可能性が高まり、バグの発生を未然に防ぐことができます。これがまさに、防御的プログラミングの第一歩です。

`ByVal`を明示的に指定するには、引数名の前に`ByVal`キーワードを付けます。

Sub MainProgram_ByValSafe()
Dim myNumber As Long
myNumber = 100 ‘ 初期値は100

Debug.Print “MainProgram実行前: myNumber = ” & myNumber ‘ 100

‘ ByValで引数を渡すプロシージャを呼び出す
Call IncrementNumber(myNumber)

Debug.Print “MainProgram実行後: myNumber = ” & myNumber ‘ 値は変わっていない!
End Sub

‘ 引数にByValを明示的に指定
Sub IncrementNumber(ByVal targetNumber As Long)
Debug.Print “IncrementNumber内(変更前): targetNumber = ” & targetNumber ‘ 100
targetNumber = targetNumber + 50 ‘ 引数の値を変更
Debug.Print “IncrementNumber内(変更後): targetNumber = ” & targetNumber ‘ 150
End Sub

このコードを実行すると、イミディエイトウィンドウには以下のように表示されます。

MainProgram実行前: myNumber = 100
IncrementNumber内(変更前): targetNumber = 100
IncrementNumber内(変更後): targetNumber = 150
MainProgram実行後: myNumber = 100

`IncrementNumber`プロシージャ内で`targetNumber`が`150`に変わっていても、呼び出し元の`myNumber`は`100`のまま保持されていますね!これで安心してプロシージャを呼び出せます。

4. 「ByRef」をあえて使うべき時、そしてその覚悟

では、`ByRef`は常に「悪」なのでしょうか?いいえ、そんなことはありません。`ByRef`には`ByRef`を使うべき正当な理由と場面があります。ただし、そこには「覚悟」が必要です。

ByRefの有効なユースケース

1. 複数の戻り値を返す必要がある場合
VBAの`Function`プロシージャは、基本的に1つの値しか返せません。しかし、`ByRef`引数を使えば、複数の変数を「変更」することで、実質的に複数の値を呼び出し元に返すことができます。

‘ ByRefで複数の値を返す例
Sub CalculateResults(ByVal inputNum As Long, ByRef result1 As Long, ByRef result2 As Long)
result1 = inputNum 2 ‘ ByRef引数1を変更
result2 = inputNum + 10 ‘ ByRef引数2を変更
End Sub

Sub TestCalculateResults()
Dim num As Long: num = 5
Dim resA As Long
Dim resB As Long

Call CalculateResults(num, resA, resB) ‘ ByRef引数に変数そのものを渡す

Debug.Print “元の数値: ” & num ‘ 5 (ByValなので変化なし)
Debug.Print “結果1: ” & resA ‘ 10 (2倍された値)
Debug.Print “結果2: ” & resB ‘ 15 (10足された値)
End Sub

2. オブジェクトのプロパティを直接操作したい場合
これは少し高度な話になりますが、`Range`オブジェクトや`Workbook`オブジェクトなどの「オブジェクト型」の引数を`ByRef`で渡すと、そのオブジェクト自体の参照が渡されます。プロシージャ内でそのオブジェクトのプロパティを変更すれば、呼び出し元のオブジェクトにも変更が反映されます。
(厳密には、オブジェクト変数自体は常に参照渡しですが、`ByVal`で渡すとその「参照のコピー」が渡されるため、プロシージャ内でオブジェクト変数を別のオブジェクトに差し替えても呼び出し元は影響を受けません。しかし、オブジェクトのプロパティ変更はどちらの場合も反映されます。この違いは奥が深いので、今は「オブジェクトそのものに変更を加えたいときはByRefが強力」とだけ理解しておきましょう。)

3. パフォーマンスを考慮する場合(大規模データ・上級者向け)
非常に大きなデータ構造(大量の配列など)を引数として渡す場合、`ByVal`で渡すとそのデータを丸ごとコピーするため、メモリ消費や処理時間に影響を与える可能性があります。この場合、`ByRef`で参照だけを渡す方がパフォーマンス上有利になることがあります。ただし、これは最適化の最終手段であり、可読性や安全性を犠牲にするリスクを伴うため、慎重な判断が必要です。初学者のうちはまず気にしなくて大丈夫です。

ByRefを使う際の「覚悟」と注意点

`ByRef`を使う場合は、以下の点に細心の注意を払う必要があります。

  • 明確な意図を持つ: 「この引数はプロシージャ内で変更される可能性がある」という明確な意図を持って`ByRef`を使うこと。
  • ドキュメント化とコメント: プロシージャのコメントに、どの引数が変更される可能性があるのかを明記する。
  • 命名規則: 変更される可能性のある引数には、その旨がわかるような命名規則(例: `io_outputValue`など)を適用することも有効です。
  • 極力避ける: 基本的には`ByVal`を優先し、本当に`ByRef`が必要な場合のみ使用するというスタンスが、バグの少ない堅牢なコードへの道です。

5. まとめと実践へのアドバイス

今日のテーマはいかがでしたか?「ByRef」と「ByVal」は、VBAプログラミングの安全性を大きく左右する重要な概念です。

  • VBAの引数はデフォルトで「ByRef(参照渡し)」である。これは「意図せぬ副作用」を引き起こし、バグの温床となる可能性がある。
  • 「ByVal(値渡し)」は、引数の値のコピーを渡すため、呼び出し元の変数を保護する
  • 基本的には、すべての引数に`ByVal`を明示的に指定する習慣をつけましょう。これにより、コードの可読性と予測可能性が高まり、デバッグの手間を大幅に減らすことができます。これは、皆さんのコードを強くするための「防御的プログラミング」の基本です。
  • `ByRef`は、複数の戻り値が必要な場合や、オブジェクトそのものを操作したい場合など、特定の目的のために使われます。その際は、明確な意図を持ち、コードのコメントなどでその旨を明記する「覚悟」が必要です。

最初は「なんでこんなに気をつけなきゃいけないんだ?」と思うかもしれませんね。しかし、この`ByRef`と`ByVal`の違いをしっかりと理解し、`ByVal`を基本とする習慣を身につけることで、皆さんのVBAプログラミングは格段に安定し、信頼性の高いものになります。

ここをクリアすれば、Excel VBAの基本はバッチリですよ!
ぜひ、今日からご自身のコードで`ByVal`を積極的に使ってみてください。そして、少しずつ、オブジェクト指向やエラーハンドリングといった次のステップにも挑戦していきましょう。

皆さんのVBAライフが、より快適で、より生産的になることを心から願っています。頑張ってくださいね!

タイトルとURLをコピーしました