コレクション反復処理の罠:なぜVBAの「要素削除」でインデックスは狂うのか
VBAにおける `Collection` オブジェクトは、日常的な業務自動化から大規模なレガシーシステムまで、ありとあらゆる場面でデータ構造の要として君臨している。配列のように事前にサイズを宣言する必要がなく、キーとインデックスの両方でアクセスできる利便性は、開発者にとって麻薬のようなものだ。
しかし、この一見シンプルに見えるコレクションの内部構造とライフサイクルを正しく理解していないがために、多くのプログラマが「実行時エラー」の暗黒面に堕ちていく。
特に「ループカウンタを用いた反復処理中の要素削除」は、VBA初学者だけでなく、中堅・ベテランを自負するエンジニアですら見落としがちな致命的な設計ミスを引き起こす温床となっている。
本稿では、VBAのコレクションが抱えるメモリとインデックスのメカニズムを解剖し、実務の現場で絶対に破綻しない極限の安全性を備えた反復処理のイディオムを提示する。
—
悪夢の始まり:なぜインデックスは「ズレる」のか
まずは、誰もが一度は書くであろう「やってはいけないアンチパターン」から目を背けずに見てみよう。条件に合致する要素をコレクションからゴッソリ削除したい時、次のようなコードを書いていないだろうか。
【アンチパターン】前から順に削る愚行
Sub RemoveAntiPattern()
Dim col As Collection
Set col = New Collection
‘ テストデータの投入 (1から5までの数値)
Dim i As Long
For i = 1 To 5
col.Add i, “Key_” & i
Next i
‘ 偶数の要素を削除しようとする (無謀な挑戦)
For i = 1 To col.Count
‘ 【警告】このアプローチは確実に破綻します
If col(i) Mod 2 = 0 Then
col.Remove i
End If
Next i
‘ 結果の確認(恐ろしいことに、一部の偶数が残る、あるいはエラーになる)
End Sub
このコードの何が問題か。VBAの `Collection` は、要素が削除されると、それ以降のすべての要素のインデックスが自動的に「前方に1つずつ詰まる(デクリメントされる)」仕様になっている。
1. `i = 1` (値: 1): 奇数なのでスルー。
2. `i = 2` (値: 2): 偶数なので `col.Remove 2` を実行。
- この瞬間、元々 `i = 3` だった値(3)が、自動的にインデックス `2` の位置に繰り上がる。
3. 次のループで `i = 3` に進む。
- この時、インデックス `3` にあるのは、元々 `i = 4` だった値(4)である。
- 先ほど繰り上がってきた「値: 3」は完全にスキップされ、検証すらされない。
結果として、条件に合致しているはずのデータが削除漏れを起こすか、最悪の場合はコレクションの要素数がループの上限(`col.Count`)を下回った時点で 「実行時エラー ‘9’: インデックスが有効範囲にありません」 が爆発する。
—
極限の解決策①:逆順ループ(Backward Iteration)
このインデックスのズレ問題に対する最も古典的かつエレガントなアルゴリズム的アプローチが、「後ろから前へ数える(逆順ループ)」である。
アーキテクチャの真髄
コレクションの末尾から先頭に向かって(`Count` から `1` へ)走査を行う。もし末尾の要素を削除したとしても、それより「前」にあるインデックスには一切影響を与えない。したがって、インデックスのズレは構造的に発生し得ない。
Sub SafeRemoveBackward()
Dim col As Collection
Set col = New Collection
Dim i As Long
‘ 初期データ投入
For i = 1 To 5
col.Add i, “Key_” & i
Next i
‘ 【推奨アプローチ】逆順ループによる安全な削除
For i = col.Count To 1 Step -1
‘ 偶数であれば削除
If col(i) Mod 2 = 0 Then
col.Remove i
‘ 補足: キーを指定して削除することも可能 (col.Remove “Key_” & i)
‘ ただし、インデックス指定の方がオーバーヘッドが少ない
End If
Next i
Debug.Print “残り要素数: ” & col.Count ‘ 正しく奇数のみ残る
End Sub
この方法はシンプルでありながら、VBAの内部ポインタ操作を最小限に抑えるため、パフォーマンスの観点からも極めて優秀である。
—
極限の解決策②:For Each と「遅延削除(Collect & Destroy)」
レガシーシステムの複雑なビジネスロジックの中には、「どの要素を削除すべきか」の判定自体が重く、逆順ループのインデックス計算すらをじれったく感じるケースがある。また、コードの可読性を最優先したい場合もあるだろう。
ここで `For Each` を使いたくなるが、`For Each` ループの最中に `Collection.Remove` を実行すると、即座に「実行時エラー:コレクションが変更されました」が発生する。VBAのコレクションは、反復中の破壊的操作に対して厳格にイテレータを無効化する仕様になっているからだ。
この制約を華麗に回避する実務的テクニックが 「遅延削除(Collect & Destroyパターン)」 である。
アーキテクチャの真髄
1. `For Each` で安全に全要素を舐め、削除対象の「キー」または「インデックス」を別枠の一時配列(あるいは別のCollection)に記録する。
2. ループを完全に抜けた後、記録したターゲットを一網打尽に削除する。
Sub SafeRemoveDeferred()
Dim col As Collection
Set col = New Collection
Dim i As Long
For i = 1 To 5
col.Add i 10, “K_” & (i 10)
Next i
‘ 削除対象のキーを保持するためのバッファ(動的配列)
Dim removeKeys() As String
Dim removeCount As Long
removeCount = -1
Dim varItem As Variant
Dim targetKey As String
‘ 第1フェーズ: 検査と対象の収集 (For Each の安全な走査)
‘ ※注意: Collectionのキーを直接For Eachで取得することはできないため、
‘ あらかじめアイテム値や規則性からキーを特定、あるいはオブジェクト参照を格納しておく必要がある。
‘ ここでは簡易的に、オブジェクトを格納したコレクションを想定する
‘ (実務ではクラスモジュールのインスタンス等を格納することが多い)
‘ ※今回は簡単のため、インデックスを逆順にする手法を推奨するが、
‘ 複雑なオブジェクトの条件判定を伴う場合は、対象のオブジェクト参照を一時コレクションに退避させるのが定石。
End Sub
オブジェクトを扱う実務環境では、削除対象のオブジェクトそのものを「一時的なゴミ箱コレクション(`garbageCol`)」に次々と `Add` していき、メインのループ終了後に、ゴミ箱側のオブジェクトを元にしてメインコレクションから `Remove` するという手法が最も堅牢である。
—
メモリ最適化とオブジェクトの明示的解放
VBAエンジニアとして上のステージに登るために避けて通れないのが、「メモリ管理とCOMオブジェクトの参照解放」の概念である。
VBAの `Collection` に独自のクラスインスタンスやExcelのシート・セル範囲(`Range`)などのCOMオブジェクトを格納した場合、`col.Remove i` を実行しただけでは、完全なメモリ解放(デストラクタの即時発動)が行われないケースがある。VBAのガベージコレクタ(参照カウント方式)の気まぐれに命運を委ねるのは、プロフェッショナルなシステムアーキテクトのすることではない。
要素を削除する際は、以下の儀式を徹底せよ。
Sub ProfessionalMemoryCleanup()
Dim col As Collection
Set col = New Collection
‘ 例: 何らかのクラスインスタンスを格納
‘ Dim cls As clsHoge
‘ Set cls = New clsHoge
‘ col.Add cls, “HogeKey”
‘ Set cls = Nothing ‘ 外部ポインタを切る
‘ — 削除時の作法 —
Dim targetKey As String
targetKey = “HogeKey”
‘ 1. 必要であればオブジェクトの終了メソッドを明示的に呼び出す
‘ col(targetKey).TerminateProcess
‘ 2. コレクションから削除
col.Remove targetKey
‘ 3. コレクション自体の完全破棄(処理終了時)
‘ Set col = Nothing を行うことで、内部に残存するすべての参照が一括解放される。
End Sub
レガシー環境(Excel 2010や、32bit版の極めて限られたメモリ空間)で長時間のバッチ処理を稼働させる場合、この「要素削除時のメモリリーク」が積み重なることで、数万件処理したあたりでExcelが突如沈黙する(メモリ不足クラッシュ)という、悪名高い現象を引き起こす。コレクションの操作は、常にメモリのライフサイクルとセットで設計しなければならない。
—
結言:VBAを「言語」としてではなく「アーキテクチャ」として捉えよ
VBAは、しばしば「おもちゃのプログラミング言語」と揶揄される。しかし、その裏側にあるCOMの仕様、メモリ管理、そして今回解説したコレクションのインデックス変動のメカニズムを正しく理解していれば、それはミッションクリティカルな業務を支える強靭なエンジンへと変貌する。
「動けばいい」というアマチュアのコードから脱却し、インデックスの奔流を完全に制御下に置くこと。それこそが、現場の信頼を勝ち得むとするシニアエンジニア、そして真のチーフアーキテクトの矜持である。
明日からのコードには、ぜひこの「逆順ループ」あるいは「安全な遅延削除」のイディオムを導入し、バグの芽を根絶やしにしてほしい。
