【Excel VBA】コレクション反復処理の罠:「要素の削除」でインデックスがズレる現象を完全に制圧する設計思想
業務自動化の現場において、`Collection` オブジェクトは非常に強力な相棒だ。動的な配列として、あるいは簡易的な連想配列(Dictionaryの軽量版)として、数々のマクロでその真価を発揮している。
しかし、シニアエンジニアへの階段を登る過程で、誰もが一度はこの壁にぶつかる。
「ループの途中でコレクションの要素を削除すると、ランタイムエラーが起息するか、データが意図せずスキップされる」という悪夢だ。
今回は、この現象のメカニズムをコードの裏側の挙動(メモリとインデックスの管理)から解き明かし、実務の現場で二度とバグを生み出さないための「堅牢な設計と実装パターン」を伝授する。
—
1. なぜ「前から順に消すループ」は破滅を招くのか
まずは、多くの開発者が最初に書いてしまいがちな「アンチパターン」を見てみよう。
【アンチパターン】前方からの走査と削除
‘ ⚠️【絶対にやってはいけない例】
Dim col As Collection
Set col = New Collection
‘ (ここに要素が10個追加されているとする)
Dim i As Long
For i = 1 To col.Count
If ConditionMet(col(i)) Then
col.Remove i ‘ ここで削除するとインデックスがズレる!
End If
Next i
一見すると筋が通っているように見えるこのコードは、条件に合致する要素を削除した瞬間に致命的なバグを引き起こす。
インデックス崩壊のメカニズム
VBAの `Collection` は、内部で要素を1から始まる連続したインデックスで管理している。
1番目の要素を削除すると、それまで2番目だった要素が自動的に「1番目」に繰り上がる。
ここで何が起きるか:
1. ループカウンタ `i = 1` のとき、1番目の要素を削除する。
2. 元々2番目だった要素が、新しい1番目の位置にシフトしてくる。
3. 次のループでカウンタが `i = 2` にインクリメントされる。
4. 結果として、シフトしてきた「元2番目の要素」が検査からスキップされる。
5. さらに、コレクションの総数(`col.Count`)が減るため、最後尾付近で `Run-time error ‘9’: インデックスが有効範囲にありません。` が爆誕する。
この「データが勝手にスキップされる」という挙動は、エラーで止まらないだけに、データ不整合という最も厄介なサイレントキラーと化す。
—
2. プロダクションコードにおける「2つの解決アプローチ」
この問題をクリアし、実務に耐えうる堅牢なコードを書くためには、以下の2つのアプローチのいずれかを採用する必要がある。
1. 逆順ループ(Backward Loop):インデックスのズレを構造的に無力化する。
2. 別コレクションへの退避(Filtering / Out-of-place):元データを汚さず、必要なものだけを再構築する。
それぞれのメリット・デメリットを理解し、現場の要件に応じて使い分けるのがプロのアーキテクトだ。
—
3. 実装コード:コピペで使える堅牢なモジュール
ここでは、実務のデータ処理(例えば、ステータスが「完了」または「無効」の項目をコレクションからパージする処理)を想定したプロダクションコードを提示する。
Option Explicit
Public Sub RunCollectionCleanupDemo()
‘ 1. テストデータの準備
Dim masterCol As Collection
Set masterCol = New Collection
‘ サンプルとして文字列を格納(実務ではオブジェクトや構造体でも同様)
masterCol.Add “KEEP_01”
masterCol.Add “DELETE_ME”
masterCol.Add “KEEP_02”
masterCol.Add “DELETE_ME”
masterCol.Add “KEEP_03”
Debug.Print “— 削除前 —”
Call PrintCollection(masterCol)
‘ 2. 堅牢な削除処理の実行(逆順ループの適用)
Call PurgeItemsBackward(masterCol, “DELETE_ME”)
Debug.Print “— 削除後(逆順ループ適用) —”
Call PrintCollection(masterCol)
End Sub
‘ ==============================================================================
‘ 方式A:逆順ループによるインプレース削除(メモリ効率重視)
‘ ==============================================================================
Private Sub PurgeItemsBackward(ByRef targetCol As Collection, ByVal targetValue As String)
Dim i As Long
‘ 【極意】後ろから前に向かってループを回す
‘ これにより、削除によってインデックスがズレても、
‘ 「すでに検査が終わった後ろ側の要素」にしか影響しないため、未検査データのスキップが防げる。
For i = targetCol.Count To 1 Step -1
If targetCol(i) = targetValue Then
targetCol.Remove i
End If
Next i
End Sub
‘ ==============================================================================
‘ 方式B:フィルタリングによる別コレクションの生成(安全性・可読性重視)
‘ ==============================================================================
Private Function FilterCollection(ByVal sourceCol As Collection, ByVal excludeValue As String) As Collection
Dim resultCol As New Collection
Dim v As Variant
‘ For Each による安全な全走査
For Each v In sourceCol
If v <> excludeValue Then
resultCol.Add v
End If
Next v
Set FilterCollection = resultCol
End Function
‘ デバッグ用ヘルパー
Private Sub PrintCollection(ByVal col As Collection)
Dim v As Variant
For Each v In col
Debug.Print v
Next v
End Sub
逆順ループ(方式A)が完璧な理由
`Step -1` を使って `Count` から `1` へ向かってデクリメントしながらループを回す場合、要素を削除しても「これから検査する要素(現在のインデックスより小さい要素)」のインデックスには一切影響を与えない。
メモリの再割り当てや新規オブジェクトの生成コストが発生しないため、数万件規模のデータを扱うバッチ処理においてもパフォーマンスを維持できる。
—
4. ファイルやデータベース連携における注意点
このコレクションの操作特性は、Excelのシート上からデータを読み込んで処理する際や、外部データベース(ADO経由のRecordset等)から取得したキャッシュをメモリ上で加工する際にも直結する。
- レコードセットのキャッシュ化
DBから取得した複数行のデータを一度 `Collection` に詰め替え、VBA側でビジネスロジックによるフィルタリングを行ってから一括してExcelシートに書き戻す、というアーキテクチャを採用する場合がある。
このとき、不要なレコードの除外ロジックで「前方ループでの削除」を書いてしまうと、特定の条件のレコードがごっそり抜け落ちるという、原因究明が困難なバグの温床になる。
- トランザクションとエラーハンドリング
コレクション操作中に予期せぬエラー(型不一致など)が発生した場合のロールバック設計も重要だ。破壊的なインプレース削除(方式A)を行う場合は、処理途中で失敗したときのリカバリが難しくなるため、データ量とメモリのトレードオフを考慮し、あえて非破壊的な「方式B(新規コレクションの生成)」を選択する勇気も必要である。
—
5. チーフアーキテクトからの総括
VBAの `Collection` はシンプルであるがゆえに、その内部構造を理解していないと手痛いしっぺ返しを受ける。
1. ループ中の要素削除は、インデックスのズレ(スキップ・エラー)を引き起こす。
2. その場で削るなら、必ず `Step -1` の逆順ループを使うこと。
3. コードの安全性を最優先するなら、新しいコレクションへ必要な要素を移し替えるフィルタリングアプローチを採用せよ。
「動けばいい」のフェーズを抜け出し、保守性が高く、誰が読んでも破綻しない美しいコードを書くこと。それこそが、現場の信頼を勝ち取るエンジニアの流儀である。明日からのコードに、ぜひこの知見を組み込んでほしい。
