鉄の掟:ForとFor Eachを使い分けよ。VB.NETのメモリ構造から紐解くループの真実
VB.NETを扱うエンジニア諸君。君たちは「とりあえずForで回せばいい」と考えていないか?
レガシーシステムの深淵を覗き込み、Windows APIの気まぐれな挙動に翻弄されてきた私から見れば、それは「爆弾を抱えて火中を歩く」に等しい。
ループ処理は単なる構文ではない。それはメモリ上に展開されたオブジェクトの海を、どう航行するかという「戦略」そのものだ。今日は、VB.NETにおけるループ処理の真髄と、例外(Exception)という名の地雷を回避するための極限の知見を授ける。
—
1. 構造の解剖:For と For Each の本質的差異
まず、君たちが使っているループの内部構造を理解せよ。
- Forループ(インデックスベース): メモリ上のアドレスに対する「直接アクセス」だ。カウンタ変数 `i` を用いて、配列やコレクションの特定要素を物理的に指し示す。速度は速いが、範囲外参照(IndexOutOfRangeException)という脆弱性を常に孕んでいる。
- For Eachループ(イテレータベース): これは「抽象化された巡回」だ。内部で `GetEnumerator()` を呼び出し、コレクションを指すポインタを順次進める。このとき、コレクションの状態が変更されると、イテレータは「裏切られた」と判断し、`InvalidOperationException` を叩きつける。
なぜ例外が発生するのか
初心者が最も陥りやすい罠は、「走査中のコレクションに対する要素の追加・削除」だ。イテレータは、ループ開始時点の「コレクションのバージョン」を監視している。ループ中に要素を増減させれば、そのバージョン番号が不整合を起こし、実行時エラーとなる。
—
2. 安全な走査のための「極限の設計」
現場で生き残るための原則はただ一つ。「イテレータを汚すな」。
走査中に要素を変更する必要がある場合は、決して元のコレクションを直接いじってはいけない。以下のコードを見てほしい。
.net
‘ 悪い例:For Each中にコレクションを直接操作(爆死するパターン)
For Each item In myCollection
If item.IsExpired Then
myCollection.Remove(item) ‘ ここでInvalidOperationExceptionが発生
End If
Next
‘ 正しい例:削除対象を別のリストに退避させる(賢者のアプローチ)
Dim itemsToRemove As New List(Of MyObject)
For Each item In myCollection
If item.IsExpired Then
itemsToRemove.Add(item)
End If
Next
‘ 走査が終わった後に一括処理する
For Each item In itemsToRemove
myCollection.Remove(item)
Next
このアプローチはメモリをわずかに消費するが、システム全体の安定性を守るための「必要経費」だ。
—
3. シニアのためのメモリ最適化:IDisposableの呪縛
VB.NETはガベージコレクタ(GC)が優秀だが、Windows APIを叩く際や、大規模な配列を扱う際は話が別だ。特にレガシーなCOMオブジェクトや、非管理リソースを扱う場合、`For Each` の背後で生成されるイテレータが、ガベージコレクションのタイミングを待たずにメモリを圧迫することがある。
実践:明示的な解放の作法
`Using` ステートメントを忘れる者は、アーキテクトを名乗る資格がない。
.net
‘ 大量のデータ処理時、イテレータを確実に解放する
Dim collection As IEnumerable(Of DataRow) = GetLargeDataFromApi()
Using enumerator As IEnumerator(Of DataRow) = collection.GetEnumerator()
While enumerator.MoveNext()
Dim item As DataRow = enumerator.Current
‘ 非管理リソースへのアクセスが必要なAPI呼び出し
ProcessData(item)
End While
‘ Usingブロックを抜ける際、明示的にリソースが解放される
End Using
—
4. アーキテクトからの提言:レガシー保守の極意
VBAからVB.NETへ移行した現場で最も多いトラブルは、「配列の境界チェックを怠ったことによる例外」だ。VB.NETでは `Option Infer On` や `Option Strict On` を強制せよ。
- 境界チェックを極限まで減らせ: `For i = 0 To array.Length – 1` は古い。`For Each` を優先せよ。コンパイラが最適化を行い、境界チェックのオーバーヘッドを最小化してくれるからだ。
- Windows APIとの連携: P/Invoke(`Declare` 構文)を使うときは、ポインタの生存期間に注意せよ。`For Each` でイテレータが指しているオブジェクトを、APIに渡す際は「固定(Pinning)」を忘れるな。
結びに代えて
技術とは、単にコードを書くことではない。「例外が発生する余地を、いかに論理的に排除するか」という、設計思想の体現である。
君たちが書くそのループ一つひとつが、将来の保守エンジニアが夜中に呼び出される原因になるかもしれないし、逆に、何年も安定して稼働し続ける伝説の礎になるかもしれない。
さあ、コードを開け。メモリの呼吸を感じろ。そして、例外に屈しない堅牢なコードを構築せよ。健闘を祈る。
