VBAでSubやFunctionを作成していると、「ByValとByRefはどちらを使えばよいのか」と迷うことがあります。
どちらも引数を渡すための構文ですが、選び方を誤ると、呼び出し元の変数が意図せず変更されたり、仕様変更時に思わぬ不具合が発生したりする原因になります。
特に実務では、プロシージャを複数のマクロから呼び出すことが多く、一度作成した処理を長期間運用するケースも少なくありません。そのため、「動けばよい」という考えではなく、「安全に呼び出せる設計」を意識することが重要です。
既にByValやByRefの基本を理解していても、「実際の業務ではどちらを選ぶべきなのか」が分からないという方も多いでしょう。
この記事では、Sub・Functionを呼び出す場面に焦点を当て、ByValとByRefの使い分け方や、実務で保守しやすい引数設計の考え方を解説します。
✅ ByValとByRefは「呼び出し側への影響」が大きく異なる
ByValとByRefの違いは「値渡しか参照渡しか」という説明だけでは、実務で使い分けるには十分ではありません。実際に重要なのは、「呼び出した側のデータを変更してよいのか」という設計です。同じ処理でもByValとByRefを切り替えるだけで、呼び出し元の変数が変更される場合と変更されない場合があります。これを理解せずにプロシージャを作成すると、他の処理へ影響を与え、原因を特定しにくい不具合につながることがあります。まずは、呼び出し元へ与える影響の違いを整理しましょう。
・ByValは呼び出し元を変更しない設計
ByValは、引数の値をコピーしてプロシージャへ渡します。
そのため、プロシージャ内で引数を書き換えても、呼び出し元の変数は変更されません。
Sub Main()
Dim salesAmount As Long
salesAmount = 100
Call UpdateValue(salesAmount)
MsgBox salesAmount '100
End Sub
Private Sub UpdateValue(ByVal targetValue As Long)
targetValue = 200
End Sub
このように、処理の中だけで値を利用したい場合はByValを選択すると安全です。
特に、入力値を参照するだけのチェック処理や計算処理では、ByValを基本とすることで、呼び出し元のデータを誤って変更するリスクを防げます。
ByValの基本構文や、複数の引数を安全に受け渡す方法から確認したい方は、「【VBA】ByValの基本と複数の値渡しの使用方法|引数の安全な扱い方」で詳しく解説しています。
・ByRefは呼び出し元の値も変更できる
ByRefでは、変数そのものを参照して処理を行います。
そのため、プロシージャ内で値を書き換えると、呼び出し元にも反映されます。
Sub Main()
Dim salesAmount As Long
salesAmount = 100
Call UpdateValue(salesAmount)
MsgBox salesAmount '200
End Sub
Private Sub UpdateValue(ByRef targetValue As Long)
targetValue = 200
End Sub
処理結果を呼び出し元へ返したい場合には便利ですが、「知らない間に値が変わる」という原因にもなります。
そのため、ByRefを使用する場合は、「このプロシージャは引数を書き換える」ということが分かる設計を意識することが重要です。
ByRefで呼び出し元の変数が変更される仕組みや、基本的な記述方法を確認したい方は、「【VBA】ByRefの基本的な使用方法|引数の参照渡しを理解する」も参考にしてください。
✅ 実務ではByValを基本に考える理由
VBAでは、引数へByValもByRefも指定できます。しかし、実務では「変更しないものはByVal」「変更するものだけByRef」という考え方が広く採用されています。その理由は、プロシージャの役割が分かりやすくなり、後からコードを読む人も安心して利用できるためです。また、仕様変更や機能追加が発生した場合でも、影響範囲を把握しやすくなります。ここでは、ByValを基本とするメリットを見ていきましょう。
・副作用を防ぎやすい
副作用とは、戻り値以外のデータまで変更してしまうことです。
例えば、入力チェックを行うだけのプロシージャで引数を書き換えてしまうと、呼び出し元のデータまで変わり、他の処理へ影響することがあります。
ByValを使用すれば、このような副作用を防ぎやすくなります。
・呼び出す側が安心して利用できる
ByValで作成されたプロシージャは、「渡したデータは変更されない」という前提で利用できます。
そのため、
同じ変数を複数回利用する
他のプロシージャへ再利用する
保守担当者がコードを読む
といった場面でも安心して呼び出せます。
読みやすいコードとは、処理内容だけでなく、「何が変更されるのか」が予測できるコードでもあります。
ByValとByRefの仕組みそのものや、値渡し・参照渡しの違いを基礎から整理したい方は、「【VBA】ByVal と ByRef の違い|値渡し・参照渡しの理解を深める完全ガイド」をご覧ください。
✅ ByRefを使うべきケース
ByRefは危険だから使わないというわけではありません。
実務でも、ByRefの方が適しているケースは数多くあります。
重要なのは、「値を書き換えること」が目的になっているかどうかです。
その意図が明確であれば、ByRefは非常に便利な構文になります。
・呼び出し元へ結果を返したい場合
複数の結果を返したい場合は、ByRefが活躍します。
例えば、
最終行を取得する
エラーメッセージを返す
処理件数を返す
といったケースでは、戻り値とは別に結果を受け渡せます。
このような用途では、ByRefを利用する方がシンプルな設計になることがあります。
・大きなデータを操作する処理
配列やコレクションなど、大量データを扱う処理では、呼び出し元のデータを更新すること自体が目的になる場合があります。
例えば、
配列を並び替える
配列へデータを追加する
Dictionaryへ値を登録する
などは、ByRefによる設計が適しています。
ただし、「更新すること」が利用者に分かるよう、プロシージャ名やコメントで意図を明確にしておくことが重要です。
✅ ByValとByRefを実務で使い分ける設計例
実務では、すべての引数を一律にByValまたはByRefへ統一するのではなく、引数ごとの役割に応じて使い分けます。検索条件や設定値など、処理が参照するだけの情報はByValで受け取り、処理結果として呼び出し元へ返す値だけをByRefにするのが基本です。この区別が明確であれば、プロシージャを呼び出しただけで元の値が変わるといった予期しない動作を防げます。
・入力値と出力値を引数の宣言で区別する
次の例では、売上金額と税率をByValで受け取り、計算結果をByRefで呼び出し元へ返しています。
Option Explicit
Private Sub CalculateSalesTax( _
ByVal salesAmount As Currency, _
ByVal taxRate As Double, _
ByRef taxAmount As Currency)
'受け取った金額は変更せず、計算結果だけを呼び出し元へ返す
taxAmount = salesAmount * taxRate
End Sub
Public Sub ExecuteSalesTaxCalculation()
Const TAX_RATE As Double = 0.1
Dim salesAmount As Currency
Dim calculatedTaxAmount As Currency
salesAmount = 10000
CalculateSalesTax _
salesAmount:=salesAmount, _
taxRate:=TAX_RATE, _
taxAmount:=calculatedTaxAmount
MsgBox "消費税額は" & Format$(calculatedTaxAmount, "#,##0円") & "です。"
End Sub
この構成では、salesAmountとtaxRateは計算に必要な入力情報、taxAmountは処理結果として返す情報です。
引数の宣言を見るだけで、それぞれの役割を判断できます。
・変更される値を限定すると影響範囲が見えやすい
すべての引数をByRefにすると、プロシージャ内でどの値が変更される可能性があるのか判断しにくくなります。
一方、変更する必要がある引数だけをByRefに限定すれば、コードを読む人は「この変数だけが呼び出し後に変わる」と予測できます。
引数の数が増えるほど、この区別は保守性に大きく影響します。
プロシージャの宣言側で定義するパラメータと、呼び出し側から渡す引数は、厳密には役割が異なります。用語の違いから整理したい方は、「【VBA】パラメータと引数の違い理解する|初心者が必ずつまずく概念を徹底解説」も参考にしてください。
✅ 1つの結果を返すならFunctionを優先する
ByRefを使えば、Subから呼び出し元へ結果を返せます。
しかし、返したい結果が1つだけであれば、ByRefよりFunctionの戻り値を使う方が処理の目的を理解しやすくなります。
・計算結果を戻り値として受け取る設計
先ほどの消費税計算は、Functionを使用して次のように書き換えられます。
Option Explicit
Private Function CalculateSalesTax( _
ByVal salesAmount As Currency, _
ByVal taxRate As Double) As Currency
'計算結果をFunctionの戻り値として返す
CalculateSalesTax = salesAmount * taxRate
End Function
Public Sub ExecuteSalesTaxCalculation()
Const TAX_RATE As Double = 0.1
Dim salesAmount As Currency
Dim calculatedTaxAmount As Currency
salesAmount = 10000
calculatedTaxAmount = CalculateSalesTax( _
salesAmount:=salesAmount, _
taxRate:=TAX_RATE)
MsgBox "消費税額は" & Format$(calculatedTaxAmount, "#,##0円") & "です。"
End Sub
この書き方では、CalculateSalesTaxが「税額を計算して返す処理」であることが呼び出し側からも分かります。
ByRefで出力先を渡す方法と比べて、結果がどこへ代入されるのかも明確です。
・戻り値を選ぶことで処理の責任が伝わる
1つの値を返すだけの処理にByRefを使用すると、呼び出し側は引数のどれが入力で、どれが出力なのかを宣言部分まで確認しなければなりません。
Functionなら、右辺で処理を呼び出し、左辺で結果を受け取るため、処理の流れを追いやすくなります。
実務では、次のように考えると整理しやすくなります。
1つの結果を返す
→ Functionの戻り値複数の結果を返す
→ 必要に応じてByRef呼び出し元のデータを直接更新する
→ 意図を明確にしたうえでByRef値を参照するだけ
→ ByVal
ByRefが使えるから使うのではなく、処理の役割が最も伝わる方法を選ぶことが重要です。
値を返すからFunctionにするのか、処理の実行を目的としてSubにするのかは、プロシージャの責任を整理して決める必要があります。詳しい判断基準は、「【VBA】SubとFunctionの違いとは?役割・使い分け・設計基準を徹底解説」で紹介しています。
Functionの戻り値を活用すると、処理結果の受け取り方が明確になり、ByRefへ出力先を渡す必要も減らせます。戻り値を使ったプロシージャ設計は、「【VBA】プロシージャの戻り値の活用方法|Function設計で処理を整理する」で詳しく解説しています。
✅ 複数の結果を返す場合はByRefが役立つ
Functionの戻り値は基本的に1つですが、実務では成功・失敗の判定と、処理結果やエラーメッセージを同時に返したいことがあります。
このようなケースでは、Functionの戻り値とByRefを組み合わせる方法が有効です。
・成功判定と取得結果を分けて返す
次のコードでは、セルの値を数値として取得できたかをBoolean型の戻り値で返し、取得した数値をByRefで返しています。
Option Explicit
Private Function TryGetNumericValue( _
ByVal targetCell As Range, _
ByRef numericValue As Double) As Boolean
'戻り値を初期化し、失敗時に前回値が残らないようにする
TryGetNumericValue = False
numericValue = 0
If targetCell Is Nothing Then Exit Function
If IsError(targetCell.Value) Then Exit Function
If Not IsNumeric(targetCell.Value) Then Exit Function
numericValue = CDbl(targetCell.Value)
TryGetNumericValue = True
End Function
Public Sub ExecuteNumericCheck()
Dim targetCell As Range
Dim numericValue As Double
Dim wasSuccessful As Boolean
Set targetCell = ThisWorkbook.Worksheets("Sheet1").Range("A2")
wasSuccessful = TryGetNumericValue( _
targetCell:=targetCell, _
numericValue:=numericValue)
If wasSuccessful Then
MsgBox "取得した数値は" & numericValue & "です。"
Else
MsgBox "A2セルから数値を取得できませんでした。"
End If
End Sub
Functionの戻り値は処理の成否、ByRefの引数は取得結果というように、役割を分けています。
単にByRefで複数の値を書き換えるより、呼び出し側で成功を確認してから結果を利用できるため、安全性が高まります。
・初期値を設定して古い値が残るのを防ぐ
ByRefで結果を返す処理では、正常終了したときだけ値を代入する書き方に注意が必要です。
呼び出し元の変数に以前の値が残っていると、処理が失敗したにもかかわらず、古い結果を新しい結果として利用してしまう可能性があります。
そのため、プロシージャの開始時に出力用の引数を初期化し、失敗時の状態を明確にしておくと安全です。
複数の結果を返す方法には、ByRefのほかにも、配列・ユーザー定義型・オブジェクトなどを活用する設計があります。実装方法の違いは、「【VBA】Functionで複数の戻り値を返す方法と活用例」で詳しく解説しています。
✅ ByValでもオブジェクトの中身は変更できる
ByValを指定すれば、すべての変更を防げるわけではありません。
RangeやWorksheetなどのオブジェクトをByValで受け取った場合、呼び出し元のオブジェクト変数そのものを別のオブジェクトへ差し替えることはできませんが、参照先のセル値やプロパティは変更できます。
Private Sub ClearTargetCell(ByVal targetCell As Range)
'ByValでも、参照先のセル内容は変更される
targetCell.ClearContents
End Sub
この処理を呼び出すと、渡したRange変数の参照先にあるセルは空になります。
ByValはオブジェクトのコピーを作成する指定ではなく、オブジェクトへの参照を値として渡す指定です。
そのため、オブジェクトを引数にする場合は、ByValかByRefかだけでなく、プロシージャ内で参照先を変更する処理があるかも確認する必要があります。
・プロシージャ名で変更処理だと伝える
オブジェクトの内容を変更するプロシージャには、次のように動作が分かる名前を付けます。
ClearTargetCellUpdateSalesWorksheetApplyReportFormatDeleteEmptyRows
ProcessDataやExecuteTaskのような広すぎる名前では、呼び出すことで何が変更されるのか分かりません。
ByValを使用していてもデータを変更する処理では、プロシージャ名やコメントによって副作用を明確にすることが重要です。
✅ ByValとByRefで起こりやすい実務上の失敗
ByValとByRefに関する不具合は、構文エラーとして表示されるとは限りません。
マクロ自体は正常終了しても、その後の処理で値が変わっていることに気付き、原因調査に時間がかかるケースがあります。
・指定を省略して意図せずByRefになる
VBAでは、引数にByValまたはByRefを記述しなかった場合、既定でByRefとして扱われます。
Private Sub UpdateQuantity(ByVal productCode As String, quantity As Long)
この宣言では、productCodeはByValですが、quantityはByRefです。
「先頭にByValを書いたから、後ろの引数にも適用される」と誤解しやすいため、すべての引数へ明示的に指定する方が安全です。
Private Sub UpdateQuantity( _
ByVal productCode As String, _
ByVal quantity As Long)
コード量は少し増えますが、引数ごとの扱いが明確になり、仕様変更時の見落としを防げます。
・ByRefを出力用だと知らずに同じ変数を再利用する
ByRefで渡した変数は、呼び出し後に内容が変更される可能性があります。
その変数を後続処理でも元の値として使用すると、意図しない結果になることがあります。
ByRefを使う場合は、入力と出力で同じ変数を使い回すのではなく、結果を受け取る専用変数を用意すると処理が分かりやすくなります。
・一時的な回避方法に頼りすぎる
ByRefの引数へ式や型の異なる値を渡すため、引数をかっこで囲んで一時的な値として渡す書き方があります。
しかし、この方法はコードを読んだだけでは意図を判断しにくく、型や参照の問題を隠してしまうことがあります。
実務では、適切な型の変数へ一度代入してから渡すか、そもそもByValで受け取る設計に変更できないかを検討する方が保守しやすくなります。
ByValとByRefを適切に設計しても、呼び出し元と呼び出し先の役割が曖昧では、処理全体が追いにくくなります。標準モジュール間でSub・Functionを呼び出す方法や、処理を分割する考え方は、「【VBA】標準モジュールの呼び出し方法とは|Sub・Functionの使い分けと実務設計」で解説しています。
✅ まとめ:引数が変更される理由を説明できる設計にする
ByValとByRefを使い分けるときは、構文上の違いだけでなく、呼び出し元のデータを変更する必要があるかを基準に考えることが重要です。
今回のポイントをまとめます。
参照するだけの入力値はByValを基本にする
呼び出し元へ変更を反映する必要がある場合だけByRefを使う
1つの結果を返す処理はFunctionの戻り値を優先する
複数の結果を返す場合はByRefを組み合わせる
ByRefの出力値は処理開始時に初期化する
オブジェクトはByValでも参照先の内容を変更できる
ByValとByRefはすべての引数へ明示する
プロシージャ名から変更内容が分かるようにする
実務で安全なコードにするには、「ByValとByRefのどちらが書きやすいか」ではなく、「この引数をプロシージャ内で変更する理由を説明できるか」で判断します。
変更する必要がない引数はByValで保護し、変更が必要な場合だけByRefを選ぶことで、呼び出し元への影響が分かりやすくなります。結果として、再利用しやすく、仕様変更にも対応しやすいVBAコードにつながります。