【Excel VBA極限の知見】For EachとFor Nextの使い分け:コレクション操作におけるパフォーマンスの最適解
こんにちは。開発プロジェクトの現場で、数百万行規模のデータ処理や複雑な業務自動化システムのアーキテクチャ設計を率いているチーフエンジニアの私だ。
Excel VBAのコードレビューをしていると、いまだに「なんとなく動くから」という理由で、すべてのループに `For Each` を使っていたり、逆にセル操作に `For Next` を無理やり適用してパフォーマンスを泥沼化させているコードを散見する。
VBAにおいて、ループの選択ミスは「実行時エラーの温床」であり、「処理速度を数倍〜数十倍遅くする最大のボトルネック」だ。
今回は、`For Each` と `For Next` のオブジェクトライフサイクルやメモリ上の挙動における違いを解剖し、実務で絶対に破綻しない「最適解」をロジカルに授けよう。
—
1. 根本思想の違い:オブジェクトの「直接参照」vs「インデックスアクセス」
まず、VBAのエンジン(VBAランタイム)が内部で何をしているのか、その本質を理解する必要がある。
`For Each` の正体:イテレータによるコレクション走査
`For Each` は、コレクションや配列の「要素(オブジェクト)」を直接指し示すイテレータ(Iterator)パターンだ。
- メリット: 記述量が少なく、構文エラーが起きにくい。インデックスの境界値(上限・下限)を意識する必要がない。
- デメリット: ループ内で対象のコレクション構造(要素の追加や削除)を変更すると、即座に致命的な実行時エラー(「オブジェクトがコレクターから削除されました」等)を引き起こすか、メモリリークの温床になる。
`For Next` の正体:インデックスによるメモリ直叩き
`For Next` は、数値のカウンター(カウンタ変数)をインクリメントしながら、メモリ上のアドレスを直接指定してアクセスする手法だ。
- メリット: 圧倒的な処理速度。配列(Array)や連続したメモリ領域に対するアクセスにおいて最強のパフォーマンスを発揮する。また、「末尾からの逆順ループ(Decrement Loop)」を使うことで、要素の動的削除という高度な処理も安全にこなせる。
- デメリット: 境界値の設定ミス(`1 To Count – 1` のようなオフバイワンエラー)によるバグを生みやすい。
—
2. 徹底比較:どちらを使うべきかの判断基準
実務において、どちらを選択すべきかの境界線は極めて明確だ。以下のマトリクスを頭に叩き込んでほしい。
| 処理対象・目的 | 推奨するループ | 理由(エンジニアリング的視点) |
| :— | :— | :— |
| ワークシート上のセル範囲 (Range) | ケースバイケース | 後述する「メモリマップの挙動」に依存。基本は一括処理(値の代入など)が最速だが、行ごとの判定が必要な場合は `For Each` が安全。 |
| 動的配列 (Variant型配列) | `For Next` | メモリ上の連続領域に対するアクセス速度は `For Next` が圧倒的。数万件のループでは数倍の差が出る。 |
| コレクション (Collection / Dictionary) | `For Each` | キーやインデックスの計算コストが不要なため、純粋な走査において最も効率的。 |
| 要素を途中で削除・追加する処理 | `For Next` (逆順) | `For Each` では構造変更が不可能。`For i = UBound To LBound Step -1` が唯一の正解。 |
—
3. 【実務の現場から】バグを根絶するプロダクションコード例
机上の空論は終わりだ。ここからは、実際の業務自動化(ファイル処理やDB連携を見据えた堅牢なデータ加工)でそのまま使えるプロダクションコードを提示する。
パターンA:大量の配列データを高速処理する (`For Next` の極意)
ファイルから読み込んだ数万件のデータを一括で加工する場合、配列に対して `For Next` を回すのが鉄則だ。
Option Explicit
Public Sub ProcessLargeArrayData()
Dim rawData As Variant
Dim i As Long
Dim startTime As Double
startTime = Timer
‘ 模擬的にアクティブシートのA1:B50000を配列に一括取得 (COMオブジェクトへのアクセスを最小化)
Dim targetRange As Range
Set targetRange = ActiveSheet.Range(“A1:B50000”)
rawData = targetRange.Value
‘ 【重要】配列の次元数が保証されているか確認
If Not IsArray(rawData) Then Exit Sub
‘ For Next による高速なインデックスアクセス
For i = LBound(rawData, 1) To UBound(rawData, 1)
‘ 例: 1列目が特定の値の場合、2列目を加工する
If rawData(i, 1) = “Target” Then
rawData(i, 2) = “Processed”
End If
Next i
‘ 処理結果をワークシートへ一括書き戻し(セルへ1つずつ書き込む愚を犯さないこと)
targetRange.Value = rawData
Debug.Print “処理完了時間: ” & (Timer – startTime) & ” 秒”
End Sub
パターンB:要素の削除を伴うコレクション/シート行の走査 (`For Next` 逆順の鉄則)
「条件に合致する行を削除する」という処理で `For Each` を使った瞬間、VBAはクラッシュするか誤作動を起こす。必ず逆順の `For Next`を使え。
Option Explicit
Public Sub RemoveSpecificRowsSafely()
Dim ws As Worksheet
Set ws = ActiveSheet
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, “A”).End(xlUp).Row
Dim i As Long
‘ 【極意】行削除・列削除を行う場合は、必ず「下から上へ(逆順)」ループを回す
‘ 上から回すと、行が詰まった際にインデックスがズレて判定漏れが発生するため。
For i = lastRow To 1 Step -1
‘ 例: A列の値が “DeleteMe” の場合に行を完全削除
If ws.Cells(i, 1).Value = “DeleteMe” Then
ws.Rows(i).Delete
End If
Next i
MsgBox “不要な行のクレンジングが完了しました。”, vbInformation
End Sub
パターンC:純粋なオブジェクトの読み取り (`For Each` のエレガントな活用)
ワークブック内のすべてのワークシート名を確認したり、Dictionaryのキーを走査するなど、「構造を変更せず、順番に参照するだけ」の処理には `For Each` が最も保守性が高く美しい。
Option Explicit
Public Sub InspectWorksheets()
Dim ws As Worksheet
‘ コレクションの走査には For Each が最適
For Each ws In ThisWorkbook.Worksheets
‘ 業務システム連携時のログ出力や存在チェックなどのユースケース
Debug.Print “シート名: ” & ws.Name & ” (状態: ” & IIf(ws.Visible = xlSheetVisible, “表示”, “非表示”) & “)”
Next ws
End Sub
—
4. チーフアーキテクトからの最終提言
VBAにおけるパフォーマンスチューニングの鉄則は、「ExcelのCOMオブジェクト(セルやシート)へのアクセス回数を極限まで減らし、メモリ上で完結させること」だ。
- 配列や一括データ処理には、メモリ上のアドレスを直接叩く `For Next` を選べ。
- オブジェクトの列挙や構造変更を伴わない安全な読み取りには、コードの意図が明確になる `For Each` を選べ。
- そして、行の削除が必要なときは「逆順の `For Next`」以外の選択肢を捨てろ。
この使い分けを徹底するだけで、あなたの書くVBAコードの品質はプロフェッショナルな水準へと劇的に飛躍する。明日からの開発現場で、ぜひ実践してほしい。
