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

スポンサーリンク

こんにちは!日々の開発、本当にお疲れ様です。
Excelマクロ(VBA)の自動記録から一歩踏み出し、本格的なVB.NETの世界へようこそ。

プログラミングを学んでいくと、必ずと言っていいほど出会うのが「コレクションの繰り返し処理(ループ)」です。リストや配列に入ったデータを順番に取り出して処理する、これは開発の基本中の基本ですよね。

でも、こんな疑問や恐怖を持ったことはありませんか?
「あれ? ループの中でリストの要素を消したら、なんかエラーが出てアプリがクラッシュしたんだけど……!」

そう、実はこれ、初学者が必ず通る「呪い」のようなトラップなんです。
今回は、VB.NETの `For Each` ループと `For` ループの裏側の仕組みを紐解きながら、安全かつ爆速でコレクションを操作するための極意を優しく解説していきますね。ここをクリアすれば、あなたのVB.NETの基本スキルは一気にプロの領域へ到達しますよ!

1. なぜ「ループ中の要素削除」でエラーが出るのか?

まずは、やってはいけない「恐怖のコード」から見てみましょう。
例えば、次のような「タスクリスト(`List(Of String)`)」があるとします。ここから「完了」という文字が含まれるタスクを綺麗さっぱり消し去りたいとします。

🚨 やってはいけないアンチパターン(InvalidOperationExceptionの罠)

‘ タスクのリストを作成
Dim tasks As New List(Of String) From {“資料作成”, “完了: メール送信”, “コードレビュー”, “完了: バグ修正”}

‘ For Eachで回しながら削除しようとする(※これはエラーになります!)
For Each task As String In tasks
If task.Contains(“完了”) Then
tasks.Remove(task) ‘ ←ここで爆発します!
End If
Next

このコードを実行すると、不意打ちのように `InvalidOperationException`(コレクションが変更されました。列挙操作は続行されない可能性があります。) という例外が発生してプログラムが強制終了します。

【図解】なぜVB.NETはブチ切れるのか?

想像してみてください。あなたが美術館の館内で、床のタイルの上を「順番に1番から最後まで歩く」という作業(`For Each`)をスタッフと約束しています。

1. あなたは2番目のタイルにいます。
2. そのタイルが汚れていたので、あなたはその場でそのタイルを引っこ抜き、後ろのタイルを前に詰めました。
3. さあ困った。スタッフが持っている「次に行くべき予定表」では、次は「3番目のタイル」ですが、タイルがズレたせいで、さっき見たはずのタイルが再び目の前に現れたり、チェックすべきタイルを見失ったりします。

VB.NETの `For Each` の裏側で働いている 「イテレータ(列挙子)」 というお目付け役は、非常に神経質です。彼らは「俺が案内している最中に、勝手にリストの大きさを変えやがったな!」と気づいた瞬間、安全のために即座に処理を中断(例外をスロー)させるのです。これがエラーの正体です。

2. 解決策①:逆順 `For` ループで華麗に回避する

「じゃあ、どうすればいいのさ?」という話ですね。
リストのサイズが変わっても影響を受けないように、インデックス(何番目か)を直接指定して、しかも「後ろから前へ(逆順)」ループを回すというテクニックが、古くから伝わるシブい解決策です。

💡 安全なコード:逆順 `For` ループ

Dim tasks As New List(Of String) From {“資料作成”, “完了: メール送信”, “コードレビュー”, “完了: バグ修正”}

‘ リストの最後尾(Count – 1)から、先頭(0)に向かってマイナス方向にループする
For i As Integer = tasks.Count – 1 To 0 Step -1
If tasks(i).Contains(“完了”) Then
‘ 後ろから消していけば、前にいる未チェックの要素のインデックスは狂わない!
tasks.RemoveAt(i)
End If
Next

‘ 結果確認
For Each t In tasks
Console.WriteLine(t) ‘ 出力: 資料作成, コードレビュー
​Next

【ここがポイント!】
後ろの要素(インデックス3)を消しても、それより前にある要素(インデックス0や1)の番号は微動だにしません。だから、エラーが起きずに安全に削除できるわけです。マクロの時代から生きるベテランエンジニアもよく使う、実用的で手堅いテクニックです。

3. 解決策②:LINQ(`RemoveAll`)でスマートにワンライナー解決

現代のVB.NET(.NET Framework / .NET Core)を使っているなら、もっとエレガントな方法があります。
それは、リストが標準で持っている `RemoveAll` メソッドを使う方法です。

💎 モダンなコード:`RemoveAll` の神髄

Dim tasks As New List(Of String) From {“資料作成”, “完了: メール送信”, “コードレビュー”, “完了: バグ修正”}

‘ 「”完了” を含むもの」を条件にして、一発で全削除!
tasks.RemoveAll(Function(t) t.Contains(“完了”))

たったこれだけです! ループを書く必要すらありません。裏側の複雑なメモリ操作はVB.NETが最適化してやってくれるため、バグの入り込む隙がありません。コードの行数も減り、可読性も爆上がりします。これが現代のモダンなVB.NET開発のスタイルです。

4. パフォーマンスと使い分けの極意

最後に、エンジニアとして知っておくべき「パフォーマンス」のお話を少しだけ。

  • `For Each` のメリット: 読み取り専用(表示や集計など)のループにおいては、最も高速で安全、かつ書きやすい最高の構文です。「見るだけ」のときは迷わず `For Each` を使いましょう。
  • 要素の追加・削除をする場合: `For Each` の中では絶対にやってはいけません。
  • 複雑な条件や高速性を求めるなら LINQ / `RemoveAll`
  • ループ内でゴリゴリとインデックスをいじる制御が必要なら 逆順 `For` ループ

この使い分けが自然とできるようになれば、もう「コレクション操作でバグらせる初心者」からは卒業です。

まとめ

いかがでしたでしょうか?
今回は、VB.NETのループ処理における最大の罠「コレクション走査中の要素削除」のメカニズムと、その回避策について解説しました。

  • `For Each` は「見るだけ」のスペシャリスト。中に要素の追加・削除を入れると怒られる。
  • 消したいときは、後ろから `For` で回す か、`RemoveAll` で一網打尽にする

ここをクリアできれば、あなたの書くVB.NETコードの安全性と美しさは見違えるほど向上します。ぜひ、明日の開発現場で試してみてくださいね。

それでは、また次回の知見でお会いしましょう!

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