【実務・中級編】初心者向け:VB.NETにおけるFor EachループとForループのパフォーマンスと安全性:コレクション走査中の要素削除バグを防ぐ – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【VB.NET極限講座】For Eachの罠:コレクション走査中の要素削除バグを完全制圧する

業務自動化ツールの開発現場において、データグリッド、ファイルリスト、あるいはデータベースから取得したデータテーブルの「走査と不要データのパージ(削除)」は、避けて通れないルーチンワークだ。

しかし、初学者のみならず、中堅プログラマですさえも、ここで「コレクションの変更に関する例外(InvalidOperationException)」や、「削除漏れ(インデックスのズレ)」という致命的なバグを踏み抜く。

今回は、VB.NETにおける `For Each` ループと `For` ループの挙動の本質を紐解き、メモリとランタイムの裏側で何が起きているのかをロジカルに解説しながら、実務で絶対に破綻しない堅牢なコードパターンを伝授する。

1. なぜ「ループ中の削除」でバグが起きるのか?

まずは、現場でよく見かける「やってはいけないアンチパターン」から目を背けずに見ていこう。

敗北パターンA:For Each ループでの要素削除

.net
‘ 【厳禁】動いている最中のコレクションを勝手に書き換えてはいけない
Dim userList As New List(Of String) From {“Yamada”, “Tanaka”, “Sato”, “Suzuki”}

For Each name As String In userList
If name.StartsWith(“S”) Then
userList.Remove(name) ‘ ここで爆発する
End If
Next

結末: 実行した瞬間、`InvalidOperationException`(コレクションが変更されました。列挙操作は実行されない可能性があります。)がスローされ、アプリケーションはクラッシュする。

理由: `For Each` の裏側では、コレクションの要素を安全に順番に取り出すための「イテレータ(Enumerator)」という番人が監視を行っている。この番人は、走査中にコレクションの要素数が変わると、「データの整合性が壊れた」と判断して即座に例外を発生させる仕様になっているのだ。

敗北パターンB:前方からの For ループでの要素削除

.net
‘ 【厳禁】インデックスがズレてデータをすり抜ける悪夢
For i As Integer = 0 To userList.Count – 1
If userList(i).StartsWith(“S”) Then
userList.RemoveAt(i) ‘ 削除すると後ろの要素が前に詰められる
End If
Next

結末: 例外こそ出ないが、「Sato」が削除された瞬間に後ろの「Suzuki」がインデックス `2` から `1` へシフトアップする。その結果、ループカウンタ `i` が次に進んだとき、「Suzuki」がチェックから漏れて生き残る。データ欠損・すり抜けという、最も気づきにくいサイレントバグの完成である。

2. 正解へのアプローチ:実務で使える2つの鉄則

では、どう書くべきか。答えはシンプルだ。「逆順ループ」を使うか、「LINQ(Where句)」で新しくコレクションを作り直すか、あるいは「安全なメソッド」を使うかである。

アプローチ1:For ループの「逆順(Countdown)」走査

もしインメモリの `List(Of T)` などでパフォーマンスを極限まで追求し、無駄なオブジェクトを生成したくない場合は、後ろから前へループを回す。

  • 後ろから削除していけば、今より前のインデックスには一切影響を与えない。インデックスのズレ問題が構造的に発生しないのだ。

アプローチ2:LINQ または `RemoveAll` の活用

VB.NETのモダンな書き方として、条件に合致するものを一括削除する `RemoveAll` メソッド、あるいは条件に合うものだけを抽出した新しいコレクションを生成するアプローチが最も安全で保守性が高い。

3. 【コピペOK】プロダクション品質の堅牢コード例

実際の業務システム(ファイル処理やDB連携を想定)でそのまま使える、堅牢な実装パターンを提示する。

.net
Imports System.Collections.Generic
Imports System.IO

Module CollectionSafetySample

Sub Main()
‘ — シナリオ:ログファイル一覧から、指定日数より古い古いファイルを特定して安全に処理する —

‘ ダミーのファイルパスクラスを想定
Dim filePaths As New List(Of String) From {
“C:\Logs\app_20230101.log”,
“C:\Logs\app_20241015.log”,
“C:\Logs\error_20230501.log”,
“C:\Logs\app_20241020.log”
}

Console.WriteLine(“=== 処理前ファイル数: ” & filePaths.Count & ” ===”)

‘ ———————————————————
‘ パターン1: List(Of T).RemoveAll を使った最もスマートな一括削除
‘ ———————————————————
‘ 2023年製の古いログを一網打尽に削除する(ラムダ式による安全な評価)
Dim removedCount As Integer = filePaths.RemoveAll(Function(path) path.Contains(“2023″))

Console.WriteLine($”[RemoveAll] {removedCount} 件の古いログをパージしました。”)

‘ ———————————————————
‘ パターン2: 逆順 For ループによる厳密な制御が必要な場合
‘ ———————————————————
‘ 例:要素を削除するだけでなく、削除時に外部リソース(ファイル削除など)のログ出力や
‘ 複雑な条件分岐を伴うため、1件ずつ確実に制御したい場合。

‘ 再度ダミーデータを投入
filePaths = New List(Of String) From {
“C:\Temp\data_old.tmp”,
“C:\Temp\data_new.tmp”,
“C:\Temp\temp_old.tmp”
}

‘ 【重要】Count – 1 から 0 へ向かってデクリメントする
For i As Integer = filePaths.Count – 1 To 0 Step -1
Dim currentPath As String = filePaths(i)

‘ 条件判定(例: “old” という文字列を含むファイルを対象とする)
If currentPath.Contains(“old”) Then
‘ 業務処理:実際にはここで File.Delete(currentPath) などを安全に行う
Console.WriteLine($”[逆順ループ] 削除対象を検出: {currentPath}”)

‘ 安全に要素をインデックス指定で削除
filePaths.RemoveAt(i)
End If
Next

Console.WriteLine(“=== 処理完了後の安全なコレクション走査終了 ===”)
Console.ReadLine()
End Sub

End Module

4. チーフアーキテクトからの実践アドバイス

1. 基本は `RemoveAll` や LINQ を優先せよ
手動でインデックスを操作するコードは、バグの温床になりやすい。可読性と安全性(Safety First)の観点から、まずはフレームワーク側が提供する最適化されたメソッド(`List(Of T).RemoveAll` や `Where` 句によるフィルタリング)が使えないかを検討すること。
2. データベースやファイルI/Oとの組み合わせ
データベースのレコードセット(`DataTable` や `DataRow` のコレクション)を操作する場合も同様の罠がある。特に `DataRowCollection.Remove` をループ内で行う場合は、必ず `DataRow` の配列に一度 `.ToArray()` でコピーしてから回すか、逆順ループ、あるいは `DataTable.Select()` による抽出アプローチを取るべし。
3. パフォーマンスの過度な早まった最適化を避ける
「メモリを節約したいから」という理由だけで、不慣れな生のリソースループを書いてバグを埋め込むコストの方が、現代のハードウェアスペックにおいては圧倒的に高くつく。

堅牢なコードとは、小手先のテクニックで作るものではなく、「言語仕様とランタイムの挙動を正しく恐れ、それを逆手に取った設計」によってのみ生み出される。
今日の知見をあなたのプロジェクトの自動化スクリプトに組み込み、ワンランク上のエンジニアとして成果を上げてほしい。

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