目次
はじめに
先日、私が書いた記事のコメント欄で「VBAではByValでも配列とオブジェクトは値が変わる」という情報をいただきました。本当なのか気になったので、実際に検証してみることにしました。
VBAでByValを使えば引数の変更が呼出し元に影響しないと考えている方も多いと思いますが、データ型によって動作が異なる可能性があります。この記事では、数値、配列、オブジェクトのそれぞれでByValの動作を確認していきます。
ここでいう「オブジェクト」とは、Range、Collection、Dictionary、Workbookなど、Setを使って変数に代入するデータのことです。
コメントを頂いた記事はこちらです。
この調査の参考記事はこちらです。
ByValの基本的な動作
まずは基本的な数値型の例で、ByValの動作を確認してみます。
Sub TestByVal()
Dim num As Long
num = 10
Debug.Print "変更前: " & num ' 10
Call ChangeValue(num)
Debug.Print "変更後: " & num ' 10(変わらない)
End Sub
Sub ChangeValue(ByVal arg As Long)
arg = 999
End Sub
実行結果
変更前: 10
変更後: 10
このコード(TestByVal)を実行すると、プロシージャ内でargに999を代入しても、呼出し元のnumは10のままです。これがByValの期待される動作です。数値型の場合、値のコピーが渡されるため、関数内での変更は呼出し元に影響しません。
配列でも試してみた
次に、配列をByValで渡した場合はどうなるか試してみます。
【固定サイズ配列の場合】
Sub TestByValArray()
Dim arr(1 To 3) As Long
arr(1) = 10
arr(2) = 20
arr(3) = 30
Debug.Print "変更前: " & arr(1)
Call ChangeArray(arr)
Debug.Print "変更後: " & arr(1)
End Sub
Sub ChangeArray(ByVal arg As Variant)
arg(1) = 999
End Sub
実行結果
変更前: 10
変更後: 10
固定サイズ配列をByValで渡した場合、配列の要素を変更しても呼出し元には影響しませんでした。
【動的配列の場合】
動的配列でも同じ結果になるか確認してみます。
Sub TestByValDynamicArray()
Dim arr As Variant
arr = Array(10, 20, 30)
Debug.Print "変更前: " & arr(0)
Call ChangeDynamicArray(arr)
Debug.Print "変更後: " & arr(0)
End Sub
Sub ChangeDynamicArray(ByVal arg As Variant)
If IsArray(arg) Then
arg(0) = 999
End If
End Sub
実行結果
変更前: 10
変更後: 10
動的配列も同様に、ByValで渡せば要素の変更は呼出し元に影響しませんでした。
Excel VBAでは、配列をByValで渡した場合、配列全体がコピーされるため、要素を変更しても呼出し元には影響しないようです。
情報求む
私が試した範囲では、Excel VBAで配列をByValで渡して値が変わるケースは見つかりませんでした。もし「こういう場合は配列でも値が変わる」という例をご存知の方がいらっしゃいましたら、コメント欄で教えていただけると嬉しいです。
オブジェクトの場合は値が変わる
配列とは異なり、オブジェクトをByValで渡すと、プロパティや内容の変更が呼出し元に反映されます。
【Rangeオブジェクトの場合】
Sub TestByValRange()
Dim rng As Range
Set rng = Range("A1")
rng.Value = "元の値"
Debug.Print "変更前: " & rng.Value
Call ChangeRange(rng)
Debug.Print "変更後: " & rng.Value
End Sub
Sub ChangeRange(ByVal arg As Range)
arg.Value = "変更された値"
End Sub
実行結果
変更前: 元の値
変更後: 変更された値
セルの値を変更すると、ByValで渡していても呼出し元のRangeオブジェクトが指すセルの値が変更されました。
【Collectionオブジェクトの場合】
Sub TestByValCollection()
Dim col As Collection
Set col = New Collection
col.Add "項目1"
Debug.Print "変更前の要素数: " & col.Count
Call AddToCollection(col)
Debug.Print "変更後の要素数: " & col.Count
End Sub
Sub AddToCollection(ByVal arg As Collection)
arg.Add "項目2"
End Sub
実行結果
変更前の要素数: 1
変更後の要素数: 2
コレクションに要素を追加すると、ByValで渡していても呼出し元のコレクションに反映されました。
【オブジェクト変数自体の代入は反映されない】
ただし、オブジェクト変数自体に別のオブジェクトを代入しても、呼出し元には影響しません。
Sub TestByValRangeReplace()
Dim rng As Range
Set rng = Range("A1")
rng.Value = "元の値"
Debug.Print "変更前: " & rng.Value
Call ReplaceRange(rng)
Debug.Print "変更後: " & rng.Value
End Sub
Sub ReplaceRange(ByVal arg As Range)
Set arg = Range("B1")
arg.Value = "新しいセルの値"
End Sub
実行結果
変更前: 元の値
変更後: 元の値
このように、ByValでは「オブジェクトの中身(プロパティや要素)」は変更できますが、「変数が指すオブジェクト自体」は変更できません。
【Dictionaryオブジェクトの場合】
Sub TestByValDictionary()
Dim dic As Object
Set dic = CreateObject("Scripting.Dictionary")
dic.Add "Key1", "値1"
Debug.Print "変更前の要素数: " & dic.Count
Call AddToDictionary(dic)
Debug.Print "変更後の要素数: " & dic.Count
End Sub
Sub AddToDictionary(ByVal arg As Object)
arg.Add "Key2", "値2"
End Sub
Dictionaryを使う場合は、参照設定で「Microsoft Scripting Runtime」を追加するとDictionary型として宣言できますが、上記のようにObject型とCreateObjectを使えば参照設定なしでも動作します。
参照設定について詳しく知りたい方は、以下の記事で解説しています。
なぜこのような動作になるのか
配列とオブジェクトで動作が異なる理由を理解するには、それぞれのデータの扱い方を知る必要があります。
【通常の変数の場合】
数値型や文字列型などの通常の変数は、変数自体が値を直接保持しています。ByValで渡すと、その値がコピーされて関数に渡されます。
変数 → 値(10)をコピー → 引数
関数内で引数を変更しても、コピーされた値が変わるだけなので、元の変数には影響しません。
【配列の場合】
Excel VBAでは配列をByValで渡すと、配列全体がコピーされます。そのため、関数内で配列の要素を変更しても呼出し元には影響しません。
変数 → 配列全体をコピー → 引数
これは私が実際に検証した結果です。配列全体がコピーされるため、要素数が多い配列をByValで渡すと、パフォーマンスに影響する可能性があります。
【オブジェクトの場合】
オブジェクト変数は、実際のオブジェクト(プロパティやメソッドを持つデータ)が格納されている場所を指し示す「参照」を保持しています。ByValで渡すと、この参照がコピーされます。
変数 → 参照をコピー → 引数
↓ ↓
└─→ 同じオブジェクト ←┘
参照がコピーされても、両方の参照が同じオブジェクトを指しているため、関数内でプロパティを変更すると呼出し元にも反映されます。
ただし、Setを使ってオブジェクト変数自体を差し替えても、それは引数のコピーが別のオブジェクトを指すようになるだけなので、呼出し元の変数には影響しません。
補足
このような動作は「参照の値渡し」や「オブジェクト共有渡し」と呼ばれます。オブジェクト変数自体(どのオブジェクトを指すか)は変更できませんが、参照先のオブジェクトは共有されているため、プロパティの変更が反映されます。
実務での注意点
【注意点1】オブジェクトのプロパティや内容の変更に注意
関数やプロシージャ内でオブジェクトのプロパティや内容を変更する場合は、それが呼出し元に影響することを意識する必要があります。
Sub ProcessRange(ByVal targetRange As Range)
' この変更は呼出し元のセルにも反映される
targetRange.Value = "処理済み"
End Sub
関数の目的が「データの検証」や「情報の取得」であれば、オブジェクトの内容を変更しないよう注意が必要です。
【注意点2】ByRefとの違いを理解する
ByRefを使うと、オブジェクト変数自体を共有します。ByRefでは参照先の変更(別のオブジェクトへの差し替え)も反映されます。
Sub TestByRef()
Dim rng As Range
Set rng = Range("A1")
rng.Value = "元の値"
Debug.Print rng.Address
Debug.Print rng.Value
Call ReplaceRangeByRef(rng)
Debug.Print rng.Address ' $B$1(差し替えられている)
Debug.Print rng.Value ' 新しいセルの値
End Sub
Sub ReplaceRangeByRef(ByRef arg As Range)
Set arg = Range("B1")
arg.Value = "新しいセルの値"
End Sub
ByValではオブジェクトの内容は変更できますが、オブジェクト変数自体(どのオブジェクトを指すか)は変更できません。
【注意点3】予期しない変更を防ぐ方法
関数内でオブジェクトの内容を変更したくない場合は、読み取り専用の操作に留めるか、必要に応じてコピーを作成します。
Sub GetRangeInfo(ByVal targetRange As Range)
' 値を読み取るだけなら問題ない
Dim cellValue As String
cellValue = targetRange.Value
Debug.Print "セルの値: " & cellValue
' 変更はしない
End Sub
セルの値を変更する必要がある場合は、別のセルにコピーしてから操作することで、元のセルへの影響を防げます。
【注意点4】配列のパフォーマンスへの影響
配列をByValで渡すと配列全体がコピーされるため、要素数が多い配列を頻繁に渡す場合はパフォーマンスに影響する可能性があります。大きな配列を扱う場合は、ByRefを使用することでコピーのオーバーヘッド(余計な処理時間)を避けられます。
' 大きな配列を扱う場合はByRefが効率的
Sub ProcessLargeArray(ByRef arr As Variant)
' 配列を参照するだけでコピーは発生しない
Dim i As Long
For i = LBound(arr) To UBound(arr)
Debug.Print arr(i)
Next i
End Sub
まとめ
実際に検証した結果、Excel VBAでは配列をByValで渡しても要素の変更は呼出し元に影響しませんでした。一方、オブジェクトをByValで渡すと、プロパティの変更が呼出し元に反映されます。この違いを理解して、意図しないデータの変更を防ぐよう注意することが大切です。