VB.NETループ処理の極意:ForとFor Eachの境界線と、現場で絶対に踏んではいけない地雷
業務自動化ツールを開発していると、ファイル走査、データベースからのデータテーブル処理、大量のUIコントロール制御など、何千・何万回と繰り返すループ処理に直面する。
ここで適当なループ構文を選択しているようでは、一流の自動化エンジニアとは言えない。
「動けばいい」の精神で書かれたコードは、データ量が増えた途端にメモリを食潰し、あるいは致命的な例外(バグ)を吐いてシステムを止める。
今回は、VB.NETの基本中の基本である `For`文 と `For Each`文 について、そのライフサイクル、パフォーマンスの重み、そして実務で必ず直面する「コレクション走査中の要素削除問題」に対する正しい処方箋を伝授する。
—
1. 根本的な違い:カウンタか、イテレータか
まずは両者の思想を理解しよう。
- `For`文(インデックス制御)
- 思想: 「何番目から何番目まで」という物理的な位置(インデックス)を指定してアクセスする。
- 特徴: 配列や `List(Of T)` など、インデックスを持つ構造に対して絶大なパワーを発揮する。逆順ループや、特定のステップ数でのスキップも自在。
- `For Each`文(列挙・イテレーション)
- 思想: 「コレクションの中にある要素を一つずつ順番にくれ」という抽象的なアプローチ。
- 特徴: 内部で `IEnumerator` を使って安全に要素をたぐる。.NETが裏側でライフサイクルを管理するため、コードが極めて簡潔になり、読みやすさが爆発的に向上する。
結論としての使い分け
- 読み取り専用で、全要素を順番に触るだけなら: 迷わず `For Each` を使え。
- インデックスを使った計算が必要、または要素の書き換え・削除が伴うなら: `For` を選べ。
—
2. 現場の地雷:コレクション走査中の要素削除
業務ツールで最も多いバグの一つが、「ループしながらリストの要素を削除する」というアプローチだ。
以下のコードを見てほしい。これは絶対にやってはいけないアンチパターンだ。
【やってはいけないバグコード】
.net
‘ NG例:For Eachで回しながら削除する
Dim items As New List(Of String) From {“apple”, “banana”, “error_item”, “cherry”, “error_item”}
For Each item As String In items
If item = “error_item” Then
items.Remove(item) ‘ ← ここで例外(InvalidOperationException)が発生する!
End If
Next
なぜエラーになるのか?
`For Each` は、内部でコレクションの「列挙子(Enumerator)」を使用している。列挙中に元のコレクションの構造(要素数)が変更されると、.NETは「データの整合性が壊れた」と判断し、即座に例外をスローして安全装置を働かせるのだ。
では、これを `For`文のインデックス制御で回避しようとして、さらにやりがちなミスがこれだ。
【やってはいけない逆順忘れのコード】
.net
‘ NG例:普通のFor文で前から削除する
For i As Integer = 0 To items.Count – 1
If items(i) = “error_item” Then
items.RemoveAt(i) ‘ ← インデックスがズレて要素の飛ばしが発生する!
End If
Next
前から削除すると、削除された瞬間に後ろにあった要素が前詰めされるため、次にチェックすべきインデックスの要素を華麗にスルー(飛ばし)してしまう。バグとしては最悪の、気付きにくいタイプだ。
—
3. プロダクションコード:安全かつ高速な設計手法
では、実務において「条件に一致する要素を削除・抽出する」にはどう書くべきか。
プロとして採用すべき、堅牢で美しいアプローチを2つ提示する。
パターンA:`For`文を使うなら「後ろから前へ(逆順ループ)」
インデックスを制御しつつ削除を行う場合、ループを末尾から先頭に向かって回すのが鉄則だ。これなら後ろが消えても、すでに処理した前方のインデックスには一切影響しない。
.net
‘ OK例:逆順Forループによる安全な削除
Dim items As New List(Of String) From {“apple”, “error_item”, “banana”, “error_item”}
‘ 最後のインデックスから 0 まで -1 ずつカウントダウン
For i As Integer = items.Count – 1 To 0 Step -1
If items(i) = “error_item” Then
items.RemoveAt(i)
End If
Next
パターンB:LINQ(`RemoveAll`メソッド)を使う【推奨】
モダンなVB.NET(.NET Framework 3.5以降 / .NET Core以降)では、リストが持つ `RemoveAll` メソッドを使うのが最もスマートであり、バグの余地がない。
.net
‘ 推奨例:LINQ / RemoveAllによる宣言的な記述
Dim items As New List(Of String) From {“apple”, “error_item”, “banana”, “error_item”}
‘ ラムダ式で条件を指定して一括削除(内部で最適化されており高速)
items.RemoveAll(Function(item) item = “error_item”)
—
4. 【実務直結】ファイル・DB連携におけるパフォーマンスの注意点
業務自動化ツールでよくあるのが、数万件のCSVファイル行データやデータベースのレコードを、ループ内で処理するケースだ。
1. `For Each` とオブジェクト生成の罠
`For Each` 自体のオーバーヘッドは極めて微小だが、ループの「中身」で無駄なオブジェクト生成(`New`)を行っていると、GC(ガベージコレクション)が頻発し、パフォーマンスが急低下する。
2. 文字列結合は `StringBuilder` を使え
ログ出力やファイル書き出しのためのループ内で、`&` 演算子による文字列結合を繰り返していないか?
.net
‘ 駄目な例:メモリ上に無数のゴミオブジェクトが生成される
Dim sbResult As String = “”
For Each row In largeDataList
sbResult &= row.ToString() & vbCrLf
Next
‘ 正しい例:StringBuilderでメモリ効率を極限まで高める
Dim sb As New System.Text.StringBuilder()
For Each row In largeDataList
sb.AppendLine(row.ToString())
Next
Dim finalResult As String = sb.ToString()
特にファイルやデータベースとやり取りするバッチ処理では、この微差が数分単位の処理時間の差となって現れる。
—
チーフアーキテクトからの総括
VB.NETのループ処理は、単なる「繰り返し」ではない。
メモリのライフサイクル、コレクションの整合性、そしてコードの保守性を左右する重要なアーキテクチャの判断だ。
- 全件読み取りなら `For Each`
- インデックス操作や動的変更なら `For`(逆順) または `RemoveAll`
- 大量データの結合には `StringBuilder`
この基本原則を体に叩き込み、大規模なデータ変更にもビクともしない、堅牢で美しい業務自動化システムを構築してほしい。
