Collection was modified の原因と直し方
foreach中にList等のコレクションの要素数を変えると発生します。反復子(イテレータ)が内部状態の不整合を検知する仕組みです。
エラーメッセージの読み方
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
System- 名前空間 — System なら標準の基本例外です
InvalidOperationException- 例外クラス — 何が起きたか。ここを検索するのが最短です
Collection was modified; enumeration operation may not execute.- 詳細メッセージ — どの値が問題だったか
at Program.Main(String[] args) in Program.cs:line 4- 発生箇所 — スタックトレースの先頭行
このエラーが出る典型パターン
パターン1
1 class Program { 2 static void Main(string[] args) { 3 var nums = new System.Collections.Generic.List<int> { 1, 2, 3 }; 4 foreach (int n in nums) { 5 if (n == 2) { 6 nums.Remove(n) ; ^ 7 } 8 } 9 } 10 }
foreach中にコレクションの要素数を変えると、内部の反復子が不整合を検知して例外を投げます。
直し方: nums.Remove(n) を System.Console.WriteLine(n) にします。
パターン2
1 class Program { 2 static void Main(string[] args) { 3 var nums = new System.Collections.Generic.List<int> { 1, 2, 3 }; 4 foreach (int n in nums) { 5 if (n == 1) { 6 nums.Add(n * 10) ; ^ 7 } 8 } 9 } 10 }
要素を追加する場合も同じです。反復中はコレクションの形を変えてはいけません。
直し方: nums.Add(n * 10) を System.Console.WriteLine(n * 10) にします。
パターン3
1 class Program { 2 static void Main(string[] args) { 3 var nums = new System.Collections.Generic.List<int> { 1, 2, 3 }; 4 foreach (int n in nums) { 5 if (n == 3) { 6 nums.RemoveAt(0) ; ^ 7 } 8 } 9 } 10 }
RemoveAtでインデックス指定で消す場合も同様です。安全に消したいならToList()でコピーしてから反復します。
直し方: nums.RemoveAt(0) を System.Console.WriteLine(nums[0]) にします。
パターン4
1 class Program { 2 static void Main(string[] args) { 3 var vals = new System.Collections.Generic.List<int> { 5, 10, 15 }; 4 foreach (int v in vals) { 5 if (v == 10) { 6 vals.Remove(v) ; ^ 7 } 8 } 9 } 10 }
foreach中に要素数を変えると、反復子が不整合を検知して例外を投げます。
直し方: vals.Remove(v) を System.Console.WriteLine(v) にします。
パターン5
1 class Program { 2 static void Main(string[] args) { 3 var tags = new System.Collections.Generic.List<string> { "a", "b" }; 4 foreach (string t in tags) { 5 if (t == "a") { 6 tags.Add("c") ; ^ 7 } 8 } 9 } 10 }
要素を追加する場合も同じです。反復中はコレクションの形を変えてはいけません。
直し方: tags.Add("c") を System.Console.WriteLine(t) にします。
よくある誤解
forループなら添字で直接アクセスするので気づきにくいですが、foreachは内部で反復子を使っており、反復中の追加・削除に非常に敏感です。
実務での勘所
安全に走査しながら要素を削除したい場合の実務での定石は、foreach (var item in original.ToList())のように、走査対象を元のコレクションのコピー(ToList()で作った新しいリスト)に切り替え、削除自体は元のコレクションに対して行う方法です。あるいは、末尾から先頭に向かって添字を減らしながら回すfor (int i = list.Count - 1; i >= 0; i--)というループなら、ある位置を削除しても、まだ訪れていない前方の要素の位置がずれないため安全に削除できます。