目次
はじめに
VBAで関数にオブジェクトを渡すと、関数内での変更が元のオブジェクトにも影響してしまうことがあります。ディフェンシブコピー(防御的コピー)という手法を使うと、元のデータを保護しながら安全に処理できます。
問題の発生例
まず、何も対策しない場合にどんな問題が起こるか見てみます。
Sub TestWithoutCopy()
Dim col As Collection
Set col = New Collection
col.Add "項目1"
col.Add "項目2"
Debug.Print "処理前の要素数: " & col.Count ' 2
Call ProcessCollection(col)
Debug.Print "処理後の要素数: " & col.Count ' 3(変わってしまう)
End Sub
Sub ProcessCollection(dataCol As Collection)
' データを確認するだけのつもりが...
dataCol.Add "処理中に追加"
End Sub
実行結果
処理前の要素数: 2
処理後の要素数: 3
ProcessCollection関数の中でCollectionに要素を追加すると、元のcolにも影響してしまいます。これは、オブジェクトを関数に渡すと参照(どこにデータがあるかを示す情報)が共有されるためです。
ディフェンシブコピーとは
ディフェンシブコピーは、元のオブジェクトを保護するために、処理前にコピーを作成する手法です。「防御的コピー」とも呼ばれ、意図しない変更からデータを守るためによく使われます。
【基本的な考え方】
元のオブジェクト → コピーを作成 → コピーを処理
↓ ↓
(変更なし) (変更される)
コピーを作成して、そのコピーに対して処理を行うことで、元のオブジェクトは変更されないまま保たれます。
ディフェンシブコピーの実装方法
【Collectionのコピー】
Collectionオブジェクトをコピーする例です。
Sub TestWithCopy()
Dim col As Collection
Set col = New Collection
col.Add "項目1"
col.Add "項目2"
Debug.Print "処理前の要素数: " & col.Count ' 2
Call ProcessCollectionSafely(col)
Debug.Print "処理後の要素数: " & col.Count ' 2(変わらない)
End Sub
Sub ProcessCollectionSafely(dataCol As Collection)
Dim tempCol As Collection
Dim item As Variant
' ディフェンシブコピーを作成
Set tempCol = New Collection
For Each item In dataCol
tempCol.Add item
Next item
' コピーを変更(元のコレクションには影響しない)
tempCol.Add "処理中に追加"
Debug.Print "コピーの要素数: " & tempCol.Count ' 3
End Sub
実行結果
処理前の要素数: 2
コピーの要素数: 3
処理後の要素数: 2
新しいCollectionを作成して、元のコレクションの要素を1つずつコピーしています。これで、tempColに要素を追加しても、元のdataColには影響しません。
【Rangeオブジェクトのコピー】
セルの範囲をコピーする場合は、別のセルにコピーするか、値だけを取り出します。
Sub TestRangeCopy()
Dim rng As Range
Set rng = Range("A1:A3")
' Transpose関数を使って縦に配置
rng.Value = Application.WorksheetFunction.Transpose(Array("データ1", "データ2", "データ3"))
Debug.Print "処理前: " & rng.Cells(1, 1).Value ' データ1
Call ProcessRangeSafely(rng)
Debug.Print "処理後: " & rng.Cells(1, 1).Value ' データ1(変わらない)
End Sub
Sub ProcessRangeSafely(dataRange As Range)
Dim tempValues As Variant
' 値をコピー(配列として取得)
tempValues = dataRange.Value
' 別のセルに出力して処理
Range("B1:B3").Value = tempValues
Range("B1").Value = "処理済み"
Debug.Print "コピー先: " & Range("B1").Value ' 処理済み
End Sub
セルの値を配列として取り出し、別のセル範囲に出力してから処理しています。これで、元のセル範囲A1:A3は変更されません。
実務での活用例
【データ検証関数での使用】
データを検証する関数では、元のデータを変更したくない場合が多いです。
Function ValidateData(dataCol As Collection) As Boolean
Dim tempCol As Collection
Dim item As Variant
' ディフェンシブコピーを作成
Set tempCol = New Collection
For Each item In dataCol
tempCol.Add item
Next item
' コピーに対してチェック処理を実行
' (元のコレクションには影響しない)
If tempCol.Count = 0 Then
ValidateData = False
Else
ValidateData = True
End If
End Function
検証処理では元のデータを読み取るだけなので、コピーを作成することで安全に処理できます。
【いつコピーが必要か】
ディフェンシブコピーが必要なケースは以下の通りです。
- 関数内でオブジェクトを変更する可能性があるが、元のデータは保持したい場合
- データ検証や分析など、読み取り専用の処理でも念のため保護したい場合
- 複数の関数で同じオブジェクトを使い回す場合
逆に、関数の目的が「データの更新」であれば、わざわざコピーを作成する必要はありません。
パフォーマンスへの影響
ディフェンシブコピーは安全ですが、データ量が多い場合はコピーの作成に時間がかかります。必要な場面でのみ使用し、パフォーマンスが重要な処理では元のオブジェクトを直接操作する方が効率的です。
まとめ
ディフェンシブコピーは、新しいオブジェクトを作成してデータをコピーすることで、元のオブジェクトを保護する手法です。データの安全性を保ちながら処理できるため、予期しないバグを防ぐのに役立ちます。