配列やオブジェクト(Range、Worksheet)の「参照渡し」特有のトリッキーな例を詳しく解説
配列やオブジェクト(Range、Worksheet)は「中身」と「参照(どこを指しているか)」が分かれます。ByRef/ByValの違いは「中身を変えるか」「参照先そのものを変えるか」で挙動がガラッと変わります。ここでは、初心者がつまずきやすい“トリッキーなポイント”だけに絞って、動く例で解説します。
配列の参照渡しで起きること
要点の整理
- 中身の変更はByValでも反映されやすい: 要素の書き換えは、呼び出し元の配列に影響することが多い。
- 配列の“参照先”の変更はByRefでしか反映されない:
ReDim、Erase、別配列の代入など「配列そのものの付け替え」はByRefでないと呼び出し元に反映されない。 - 固定長配列と動的配列で挙動が違う:
Dim a(1 To 3)(固定長)とDim a()→ReDim a(1 To 3)(動的)では、ReDimの有無や可能な操作が異なる。
例1:要素変更はByValでも反映される
Sub TestArrayByValElement()
Dim a(1 To 3) As Long
a(1) = 10
ChangeElementByVal a
Debug.Print a(1) ' → 99(中身は変わる)
End Sub
Sub ChangeElementByVal(ByVal arr() As Long)
arr(1) = 99
End Sub
VB- 解説: ByValで渡しても「配列のデータ領域」は共有されるため、要素の変更が呼び出し元に反映される。
例2:ReDimはByValだと呼び出し元に効かない
Sub TestArrayByValRedim()
Dim a() As Long
ReDim a(1 To 2)
a(1) = 1
TryRedimByVal a
Debug.Print UBound(a) ' → 2(呼び出し元は変わらない)
End Sub
Sub TryRedimByVal(ByVal arr() As Long)
ReDim arr(1 To 10) ' 呼び出し先では拡張されるが、呼び出し元には反映されない
End Sub
VB- 解説: ByValは「配列の参照情報のコピー」を渡すため、
ReDimで参照先を付け替えても呼び出し元の配列はそのまま。
例3:ReDimはByRefだと呼び出し元も変わる
Sub TestArrayByRefRedim()
Dim a() As Long
ReDim a(1 To 2)
a(1) = 1
DoRedimByRef a
Debug.Print UBound(a) ' → 10(呼び出し元も拡張される)
End Sub
Sub DoRedimByRef(ByRef arr() As Long)
ReDim arr(1 To 10)
End Sub
VB- 解説: ByRefは参照そのものを渡すため、
ReDimが呼び出し元にも反映される。
例4:Eraseの違い(配列の“消去”はByRefでしか影響しない)
Sub TestArrayErase()
Dim a() As Long
ReDim a(1 To 3)
EraseByVal a
Debug.Print LBound(a), UBound(a) ' → 1, 3(変わらない)
EraseByRef a
' 次行はエラー(範囲が無効)になることがあるので注意
' Debug.Print LBound(a), UBound(a)
End Sub
Sub EraseByVal(ByVal arr() As Long)
Erase arr ' 呼び出し先でのみ消える
End Sub
Sub EraseByRef(ByRef arr() As Long)
Erase arr ' 呼び出し元の参照も消える
End Sub
VB- 解説:
Eraseは配列の実体を解放・初期化する操作。ByValだと“ローカルでの消去”に留まる。
Rangeオブジェクトの参照渡し
要点の整理
- プロパティの変更はByValでも反映される:
.Valueや.NumberFormatなど「同じRangeが指すセルの中身」は呼び出し元と共有されるため、変更が反映される。 - 参照の付け替えはByRefでないと反映されない:
Set rng = rng.Offset(1, 0)やSet rng = Nothingといった「どのセルを指すか」を変える操作は、ByRefで渡したときのみ呼び出し元に影響する。
例5:プロパティ変更はByValでも反映される
Sub TestRangeByValProperty()
Dim rng As Range
Set rng = Sheet1.Range("A1")
ChangeValueByVal rng
' A1の値 → "OK"
End Sub
Sub ChangeValueByVal(ByVal r As Range)
r.Value = "OK" ' セルの実体が同じなので反映される
End Sub
VB- 解説: オブジェクトの“指す先”が同じなら、中身の変更は共有される。
例6:参照の付け替えはByValでは反映されない
Sub TestRangeRepointByVal()
Dim rng As Range
Set rng = Sheet1.Range("A1")
RepointByVal rng
rng.Value = "A1" ' 依然としてA1を指している
End Sub
Sub RepointByVal(ByVal r As Range)
Set r = r.Offset(1, 0) ' 呼び出し先ではA2になるが、呼び出し元のrngはA1のまま
End Sub
VB- 解説: ByValだと「参照のコピー」を受け取るため、
Setによる付け替えは呼び出し元に伝わらない。
例7:参照の付け替えを呼び出し元に反映したいならByRef
Sub TestRangeRepointByRef()
Dim rng As Range
Set rng = Sheet1.Range("A1")
RepointByRef rng
rng.Value = "Now A2" ' 呼び出し元もA2を指すようになっている
End Sub
Sub RepointByRef(ByRef r As Range)
Set r = r.Offset(1, 0)
End Sub
VB- 解説: ByRefは参照そのものを渡すため、
Setの効果が呼び出し元に届く。
例8:Nothingの扱い(参照の破棄)
Sub TestRangeNothing()
Dim rng As Range
Set rng = Sheet1.Range("A1")
SetNothingByVal rng
Debug.Print rng Is Nothing ' → False(まだ生きている)
SetNothingByRef rng
Debug.Print rng Is Nothing ' → True(呼び出し元の参照も破棄)
End Sub
Sub SetNothingByVal(ByVal r As Range)
Set r = Nothing ' ローカルだけ破棄
End Sub
Sub SetNothingByRef(ByRef r As Range)
Set r = Nothing ' 呼び出し元も破棄
End Sub
VB- 解説: 参照の破棄は「どこを指すか」を変える操作の一種。ByRefでないと呼び出し元に影響しない。
Worksheetオブジェクトの参照渡し
要点の整理
- 状態変更はByValでも反映される:
.Name変更、.Range(...)の内容変更などは共有される。 - 参照の差し替えはByRefでないと反映されない:
Set ws = Worksheets("Sheet2")のような付け替えはByRefが必要。 - Activate/Selectは副作用大: 画面上のアクティブシートが変わるため、他コードへの影響が出やすい。ByRefだと「参照差し替え」と組み合わさって混乱しがち。
例9:名前変更はByValでも反映される
Sub TestWorksheetRename()
Dim ws As Worksheet
Set ws = Sheet1
RenameByVal ws
Debug.Print Sheet1.Name ' → "Data"(変更される)
End Sub
Sub RenameByVal(ByVal w As Worksheet)
w.Name = "Data"
End Sub
VB- 解説: 同じシートを指している限り、状態変更は共有される。
例10:参照の差し替えはByRefが必要
Sub TestWorksheetRepoint()
Dim ws As Worksheet
Set ws = Sheet1
RepointWsByVal ws
Debug.Print ws.Name ' → Sheet1(変わらず)
RepointWsByRef ws
Debug.Print ws.Name ' → Sheet2(参照が差し替わる)
End Sub
Sub RepointWsByVal(ByVal w As Worksheet)
Set w = Worksheets("Sheet2")
End Sub
Sub RepointWsByRef(ByRef w As Worksheet)
Set w = Worksheets("Sheet2")
End Sub
VB- 解説: ByValだと呼び出し元の
wsは元のシートを指し続ける。ByRefで参照が置き換わる。
さらにハマりやすい落とし穴
- デフォルトがByRef問題:
- 注意点: 引数に何も指定しないとByRef。意図せず参照差し替えや破棄が呼び出し元に波及する。
- 対策: 変更意図がない引数は必ずByValにする。
- オブジェクトの「中身」と「参照」を混同しない:
- 中身変更:
.Valueや.Name→ ByValでも反映。 - 参照付け替え:
Set obj = ...やNothing→ ByRefでないと反映されない。
- 中身変更:
- 配列のReDim/Eraseは“構造変更”扱い:
- 要素変更: ByValでも反映されがち。
- 構造変更:
ReDim/EraseはByRefが必要。 - 複数戻り値を配列で返したい: ByRefで配列を渡して構築するか、関数の戻り値として配列を返す(副作用を避けたいなら後者)。
- 副作用の最小化(設計指針):
- 基本方針:
- 計算・加工だけ → ByVal+戻り値(副作用なし)。
- 参照の差し替えが必要 → 明示的にByRef。
- 共有状態に触れる処理(Range/Worksheet) → 作用範囲を限定し、戻り値で新しい参照を返す設計も検討。
- 基本方針:
実務寄りの安全パターン
- 安全に新しいRangeを返す(参照を戻り値で扱う)
Sub TestReturnRange()
Dim rng As Range
Set rng = Sheet1.Range("A1")
Set rng = NextRow(rng) ' 参照の付け替えは戻り値で明示
rng.Value = "Moved"
End Sub
Function NextRow(ByVal r As Range) As Range
Set NextRow = r.Offset(1, 0)
End Function
VB- メリット: 呼び出し側が「参照の変更」を自分で受け止めるため、意図が明確で副作用が少ない。
- 配列の安全な拡張(新配列を返す)
Sub TestGrowArray()
Dim a() As Long
ReDim a(1 To 2): a(1) = 10: a(2) = 20
a = GrowArray(a, 4)
Debug.Print UBound(a) ' → 4
End Sub
Function GrowArray(ByVal src() As Long, ByVal newSize As Long) As Long()
Dim b() As Long
Dim i As Long
ReDim b(1 To newSize)
For i = LBound(src) To UBound(src)
b(i) = src(i)
Next
GrowArray = b
End Function
VB- メリット: 呼び出し元の配列を直接いじらず、新しい配列を返すため、予期しない構造変更を防げる。
まとめの指針
- 参照を付け替える可能性があるならByRef、そうでないならByVal。
- “中身の変更”はByValでも伝播することがある(特にオブジェクト・配列)ため、設計時に副作用を意識する。
- 複雑な参照操作は「戻り値で新参照を返す」パターンが安全で読みやすい。


