【テクニカル・上級編】For EachとFor Nextの使い分け:コレクション操作におけるパフォーマンスの最適解 – Excel VBA解析バイブル

スポンサーリンク

Excel VBAを掌握する極限の知見:For EachとFor Nextの使い分け、パフォーマンスの最適解

数百万行のデータ、複雑に絡み合った外部COMオブジェクト、そして際限なく肥大化するワークシート。現場のエンジニアが直面するVBAのパフォーマンス限界は、多くの場合、不適切なループ構造の選択に起因する。

「とりあえず `For Each` で回しておけば安全だ」
「インデックスアクセスの方が速い気がするから `For Next` にしよう」

もし、あなたがこのような曖昧な基準でループを選んでいるなら、今すぐその手を止めてほしい。VBA(正確にはその背後にあるCOMランタイムとメモリ管理機構)の挙動を熟知する者にとって、`For Each` と `For Next` は、それぞれ全く異なるメモリライフサイクルと最適化戦略を持つ別次元の制御構文である。

本稿では、コレクション操作における両者の本質的な違いを、メモリ効率、COMオブジェクトのオーバーヘッド、そして実務の現場で生き残るための最適解の観点から徹底的に解剖する。

1. 内部構造の比較:なぜパフォーマンスに劇的な差が生まれるのか

まず、両者のプリミティブな挙動を理解しなければならない。

`For Next`:インデックスベースのダイレクトアクセス

`For Next` は、メモリ上の連続したアドレス、あるいは配列のインデックスを直接指定して値にアクセスする。
特に、VBAの `Variant` 型配列や、`Range.Value` を一度メモリ上の二次元配列(`Variant()`)に格納したアンマネージドなメモリ領域へのアクセスにおいて、`For Next` は圧倒的な速度を発揮する。余計な抽象化層を挟まないため、CPUキャッシュ効率も極めて高い。

`For Each`:IEnumVARIANT インターフェイスの呪縛

一方、`For Each` は裏側でCOMの列挙子(Enumertor)、すなわち `IEnumVARIANT` インターフェイス を叩いている。
コレクションや Range オブジェクトに対して `For Each` を実行すると、VBAランタイムは内部で以下の処理を行っている。

1. 対象オブジェクトから `IEnumVARIANT` のインスタンスを取得する。
2. ループの各イテレーションで `Next` メソッドを呼び出し、次の要素へのポインタを取得する。
3. ループ終了時に列挙子を解放する。

この「抽象化の代償」として、純粋な数値インデックスのループと比較して、呼び出しごとのオーバーヘッド(COMのメソッドディスパッチ)が確実に上乗せされる。

2. ベンチマークから見る真実:セル操作 vs メモリ内配列

では、実際の業務で多用される「セル範囲の走査」を例に、どちらが最適解となるのか。
結論から言えば、「どこを走査するか」 によって正解は180度変わる。

2.1 ワークシートのセル(Range)を直接叩く場合

ワークシート上のセルに `For Each` でアクセスする場合、セル一つひとつのCOMオブジェクト生成コストが発生するため、極めて低速になる。しかし、これは `For Next` を使ってもセルアクセス自体のオーバーヘッド(ExcelのCOM境界を跨ぐコスト)が大きすぎるため、大差はない。

真の解決策は、セルを直接ループさせることではなく、一度メモリ内配列(Variant)に一気呵成に読み込み、配列を `For Next` で回すことである。

2.2 実践:極限まで最適化された配列走査コード

以下に、数万行のデータをミリ秒単位で処理するための、メモリ最適化されたパターンを示す。

Option Explicit

Sub UltimateArrayProcessing()
Dim ws As Worksheet
Set ws = ActiveSheet

Dim rngTarget As Range
‘ 最終行を動的に取得(UsedRangeの罠を回避するためCtrl+End相当の取得を採用)
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, “A”).End(xlUp).Row

If lastRow < 2 Then Exit Sub Set rngTarget = ws.Range("A2:D" & lastRow) ' 【重要】ワークシート上のセルアクセスを排除し、一括してメモリ(Variant配列)へ転送 Dim vData As Variant vData = rngTarget.Value Dim i As Long, j As Long Dim lUBound1 As Long, lUBound2 As Long lUBound1 = UBound(vData, 1) lUBound2 = UBound(vData, 2) ' 【最適解】メモリ上の二次元配列に対する高速な For Next 走査 For i = 1 To lUBound1 ' 例として1列目の値に処理を施す If vData(i, 1) = "Target" Then vData(i, 2) = vData(i, 1) 1.05 ' ※データ型に応じた処理 End If Next i ' 処理結果をワークシートへ一括書き戻し(Bust Writeの回避) rngTarget.Value = vData ' オブジェクト変数の明示的解放(メモリリークの完全防止) Set rngTarget = Nothing Set ws = Nothing End Sub ---

3. `For Each` が唯一無二の最適解となる領域

ここまで読むと、「`For Next` と配列が最強であり、`For Each` は悪である」という印象を受けるかもしれない。しかし、それは大きな誤りだ。

`For Each` が圧倒的な優位性を持つ聖域が存在する。それが 「コレクションオブジェクト(`Worksheets`, `Shapes`, `Dictionary`, `Collection` など)」の走査 である。

コレクションのインデックスアクセスの罠

例えば、ワークシート上のすべての図形(`Shape`)をループで処理したいとする。これを `For Next` で書くとどうなるか。

‘ 【アンチパターン】これはバグの温床となる
Dim i As Long
For i = 1 To ws.Shapes.Count
If ws.Shapes(i).Name Like “Temp_” Then
ws.Shapes(i).Delete
‘ 削除によってインデックスがズレ、次の要素をスキップするかエラーになる!
End If
Next i

コレクションから要素を動的に削除・追加する場合、`For Next` でインデックスを指定すると、インデックスのシフトによる致命的なバグを引き起こす。さらに、`ws.Shapes(i)` はインデックスアクセスに見えて、裏側では先頭からの線形探索(O(N)のコスト)を行っているケースが多く、トータルの計算量が $O(N^2)$ に跳ね上がる。

`For Each` による安全かつ高速なオブジェクト列挙

これに対し、`For Each` は内部の列挙子(`IEnumVARIANT`)が現在のポインタを確実に保持するため、インデックスのズレを意識する必要がない。

‘ 【最適解】コレクション操作における For Each の正しい姿
Dim shp As Shape
Dim shpCol As Collection
Set shpCol = New Collection

‘ 削除対象を一度別のコレクションや配列に退避する安全設計
For Each shp In ws.Shapes
If shp.Name Like “Temp_” Then
shpCol.Add shp
End If
Next shp

‘ 走査後に安全に一括削除
Dim targetShp As Shape
For Each targetShp In shpCol
targetShp.Delete
Next targetShp

‘ 明示的解放
Set shp = Nothing
Set targetShp = Nothing
Set shpCol = Nothing

4. チーフアーキテクトが直言する「使い分けの鉄則」

現場のアーキテクチャ設計において、迷った時は以下のマトリクスを思い出してほしい。

| 走査対象 / データ構造 | 推奨するループ構文 | 理由・アーキテクチャ上の根拠 |
| :— | :— | :— |
| 二次元配列 (`Variant()`) | `For Next` | メモリ上の連続領域に対する最速のポインタアクセス。オーバーヘッドゼロ。 |
| Worksheetのセル範囲 (Range) | 配列化して `For Next` | COM境界を跨ぐコストを最小化するため、一度 `Value` を配列に落とす。 |
| ネイティブコレクション
(`Worksheets`, `Collection`, `Dictionary`) | `For Each` | インデックスの動的変動に強く、線形探索のパフォーマンスペナルティを回避。 |
| 可変長配列 (`ReDim Preserve` 頻発) | 避けるべき | ループ内で配列サイズを変更する設計自体がアーキテクチャの破綻。事前確保が鉄則。 |

5. レガシー環境とメモリ管理の極意

最後に、VBAエンジニアが忘れがちな「メモリのライフサイクル」について言及する。
特に古いExcel環境や、長期間稼働するExcelプロセスにおいて、オブジェクト変数の解放漏れは、VBAランタイムのヒープ領域をじわじわと蝕み、最悪の場合は「メモリ不足(Error 7)」による突然のクラッシュを引き起こす。

`For Each` でオブジェクト型変数をイテレートする場合、ループのスコープを抜けた後も、VBAの仕様上、ポインタが残存することがある。
ループの最後、あるいはプロシージャの終了時には、必ず以下のように 明示的な `Nothing` 代替 を行うこと。

Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
‘ 処理
Next ws
Set ws = Nothing ‘ ← ここを怠るな

この小さな一手間が、数日間無人で稼働する夜間バッチ処理の安定性を担保する。

総括

技術に「なんとなく」は許されない。
`For Next` はメモリとインデックスの支配者であり、`For Each` は抽象化されたコレクションの調停者である。
それぞれの特性を深く理解し、データ構造の本質に見合った正しいループを選択することこそが、あなたの書くVBAコードを「おもちゃのマクロ」から「堅牢なエンタープライズ・システム」へと昇華させる唯一の道である。

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