foreachを使っている箇所でこんなエラーが出た場合の対処法です。
System.InvalidOperationException: 'コレクションが変更されました。列挙操作は実行されない可能性があります。'
原因:foreachで「System.InvalidOperationException コレクションが変更されました。列挙操作は実行されない可能性があります。」が出た時の対処法
まずは、なんで以下のエラー(例外)が発生したのか?について。
「System.InvalidOperationException: 'コレクションが変更されました。列挙操作は実行されない可能性があります。'」
考えられる原因は以下。
原因:foreach文で列挙中にコレクション要素を追加または削除する操作を行った。
サンプルソースで見てみましょう。
これは、あえて以下の例外を発生させるためのソースです(悪い例)です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | private void TestFunc() { // まずはテスト用のListを作成する。 List<int> list = new List<int>(); for (int i = 0; i < 10; i++) { list.Add(i); } // 先ほど作成したリストでforeach文を作成。 // foreach文の途中で要素を削除してみる。 int count = 0; foreach (int i in list) { if (count == 6) { // Indexが6の要素を削除してみる。→ ここで例外発生!!! list.RemoveAt(count); } count++; } } |
「System.InvalidOperationException コレクションが変更されました。列挙操作は実行されない可能性があります。」
これを実行してみると「list.RemoveAt(count);」のところで例外が発生するのが分かります。
つまり、foeach文の途中で、RemoveAtでリスト要素を削除しています。
この、削除処理の際に例外が発生します。
そうです、「foeach文で列挙した要素を何削除しとんねん! 」という事で例外が発生するのです。
【対処法】foreachで「System.InvalidOperationException コレクションが変更されました。列挙操作は実行されない可能性があります。」が出た時の対処法
それでは、どうやってこの例外を対処すればよいか!についてです。
3つあります。
- for文を使う!
- リストのコピーを作る!
- LINQ Whereを使う!
【対処法1】for文を使う
1番簡単なのが、for文を使う方法。
for文を使った方法でも注意するポイントがあります!まずはダメなソースコード例を紹介。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private void TestFunc() { List<int> list = new List<int>(); for (int i = 0; i < 10; i++) { list.Add(i); } // for文の繰り返し回数を算出 int length = list.Count; for (int i = 0; i < length; i++) { // 「list」の要素をDebug文で出力 Debug.WriteLine(list[i]); if (i == 6) { // indexが6の要素を削除する list.RemoveAt(i); } } } |
これを実行すると、「i が 9 の時に「Debug.WriteLine(list[i]);」で例外が発生します。
※インデックスが範囲を超えている例外。
そして、正しいソースコード例が以下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private void TestFunc() { List<int> list = new List<int>(); for (int i = 0; i < 10; i++) { list.Add(i); } // for文の繰り返し回数を算出 int length = list.Count; for (int i = length - 1; i >= 0; i--) { // 「list」の要素をDebug文で出力 Debug.WriteLine(list[i]); if (i == 6) { // indexが6の要素を削除する list.RemoveAt(i); } } } |
先ほどのNG例との差分は、以下。
1 | for (int i = length - 1; i >= 0; i--) |
ポイント
for文を「++(インクリメント)」で回すのでなく、「--(デクリメント)」で回す。
インクリメントしながら、ListからRemoveAtすると、削除したタイミングで要素のインデックスが変わってきてしまいます。
※参照する要素が途中で変わり不具合の原因になります。
それに比べ、デクリメントの場合はListを最後尾から参照していくので、途中で要素削除しても影響をうけません。
【対処法2】別のリストに結果を格納する
もう一つやるとしたら、結果格納用のコレクション(List)を用意しておく、という方法。
サンプルソースはこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private void TestFunc() { List<int> list = new List<int>(); for (int i = 0; i < 10; i++) { list.Add(i); } // リストのコピーを作る List<int> resultList = new List<int>; foreach (int i in list) { if (i == 6) { // 結果保存しない } else { resultList.Add(i); } } } |
【対処法③】「Where」「RemoveAll」を使う方法
サンプルソースを以下。
今回の例では、intのListを作ってそこから要素を削除する、というかなり単純なものなので、このような方法も考えれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private void TestFunc() { List<int> list = new List<int>(); for (int i = 0; i < 10; i++) { list.Add(i); } // 方法① // 値が「6」以外のものをListとして作成 List<int> list2 = list.Where(l => l != 6).ToList(); // 方法② // list から値が6のものを削除する。 list.RemoveAll(l => l == 6); } |
方法①:Whereを使った方法
「Where(条件)」で、条件に合致したものを、別のリストとして作成する方法。
結構簡単ですね。比較的イメージもしやすいので諸初心者にも向いてます。
方法②:RemoveAllを使った方法
次は、RemoveAllで、条件に合致した要素をRemoveする方法
今回の例では、「list.Where(l => l != 6)」で、要素値が6以外のものを新規Listとして作成しています。
Whereのカッコ内の条件を、正しく設定できればかなり楽に実現できると思います。
まとめ
「System.InvalidOperationException: 'コレクションが変更されました。列挙操作は実行されない可能性があります。'」
という例外が発生した場合の、対処法を紹介しました。
今回の例は、要素がint型のもので、結構扱いやすく簡単だったと思います。
要素が本例のint型以外のものに変わっても、基本的な考えは同じです。
- for文のデクリメントで対応
- Where、RemoveAllで対応