Excel VBA | 配列とオブジェクトの参照渡しのトリッキーな挙動

VBA
スポンサーリンク

配列やオブジェクト(Range、Worksheet)の「参照渡し」特有のトリッキーな例を詳しく解説

配列やオブジェクト(Range、Worksheet)は「中身」と「参照(どこを指しているか)」が分かれます。ByRef/ByValの違いは「中身を変えるか」「参照先そのものを変えるか」で挙動がガラッと変わります。ここでは、初心者がつまずきやすい“トリッキーなポイント”だけに絞って、動く例で解説します。


配列の参照渡しで起きること

要点の整理

  • 中身の変更はByValでも反映されやすい: 要素の書き換えは、呼び出し元の配列に影響することが多い。
  • 配列の“参照先”の変更はByRefでしか反映されない: ReDimErase、別配列の代入など「配列そのものの付け替え」は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 = ...NothingByRefでないと反映されない
  • 配列の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でも伝播することがある(特にオブジェクト・配列)ため、設計時に副作用を意識する。
  • 複雑な参照操作は「戻り値で新参照を返す」パターンが安全で読みやすい。

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