List, Dictionary, Queueなどのコレクションでは、foreach文/For Eachステートメントでの列挙中に要素を追加したり削除したりする操作を行うことはできません。
このような操作を行おうとすると「列挙操作は実行されない可能性があります」というメッセージとともに例外InvalidOperationExceptionがスローされます。 これは、追加・削除を行うことで列挙中のコレクションの列挙子が指す要素の位置が変わり(列挙子が無効となる)、正しい列挙操作を継続できなくなるためです。
列挙中にInvalidOperationExceptionがスローされる例
例として、Listに格納された値のうち、奇数のみを削除するために次のようなコードを書いたとします。 このコードはInvalidOperationExceptionをスローします。
using System;
using System.Collections.Generic;
class Sample {
static void Main()
{
List<int> list = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7});
// Listから奇数の要素を削除したい
foreach (int val in list) {
if (val % 2 != 0)
list.Remove(val); // InvalidOperationExceptionがスローされる
}
// 結果を表示
foreach (int val in list) {
Console.Write("{0}, ", val);
}
Console.WriteLine();
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7})
' Listから奇数の要素を削除したい
For Each val As Integer In list
If val Mod 2 <> 0 Then list.Remove(val) ' InvalidOperationExceptionがスローされる
Next
' 結果を表示
For Each val As Integer In list
Console.Write("{0}, ", val)
Next
Console.WriteLine()
End Sub
End Class
ハンドルされていない例外: System.InvalidOperationException: コレクションが変更されました。列挙操作は実行されない可能性があります。 場所 System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource) 場所 System.Collections.Generic.List`1.Enumerator.MoveNextRare() 場所 System.Collections.Generic.List`1.Enumerator.MoveNext() 場所 Sample.Main()
これを回避するには、foreachによる列挙中はコレクションを変更しないようにするしかありません。 逆に言えば、foreachでの列挙中に列挙中のコレクションを変更しなければよいため、次のような代替手段を使うことができます。
代替方法
for文を使う
一つの代替方法として、for文などを使いインデックスを指定してコレクションを列挙・変更する方法があります。 先の例を、for文/Do Whileステートメントを使って書き換えると次のようになります。
using System;
using System.Collections.Generic;
class Sample {
static void Main()
{
List<int> list = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7});
// Listから奇数の要素を削除したい
for (int index = 0; index < list.Count; index++) {
if (list[index] % 2 != 0)
list.RemoveAt(index);
}
// 結果を表示
foreach (int val in list) {
Console.Write("{0}, ", val);
}
Console.WriteLine();
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7})
' Listから奇数の要素を削除したい
Dim i As Integer = 0
Do
If list(i) Mod 2 <> 0 Then list.RemoveAt(i)
i += 1
Loop While i < list.Count
' 結果を表示
For Each val As Integer In list
Console.Write("{0}, ", val)
Next
Console.WriteLine()
End Sub
End Class
2, 4, 6,
この方法では、コレクションを変更することにより列挙中の要素のインデックスが変わる場合がある点に注意が必要です。 追加や削除により要素のインデックスが変化することを考慮しながらループを行う必要があります。 上記の例で言えば、{1, 3, 4, 5}
のようにList内に奇数の連続があると正しく動作しないという問題があります。
削除を行うと削除した要素以降のインデックスが変わることから、上記の例ではコレクションの末尾側から削除するように処理することで問題を回避できます。
using System;
using System.Collections.Generic;
class Sample {
static void Main()
{
List<int> list = new List<int>(new int[] {1, 3, 4, 5});
// Listから奇数の要素を削除したい
// (削除によってインデックスが変わるため、影響を避けるためListの末尾側から削除する)
for (int index = list.Count - 1; 0 <= index; index--) {
if (list[index] % 2 != 0)
list.RemoveAt(index);
}
// 結果を表示
foreach (int val in list) {
Console.Write("{0}, ", val);
}
Console.WriteLine();
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim list As New List(Of Integer)(New Integer() {1, 3, 4, 5})
' Listから奇数の要素を削除したい
' (削除によってインデックスが変わるため、影響を避けるためListの末尾側から削除する)
For index As Integer = list.Count - 1 To 0 Step -1
If list(index) Mod 2 <> 0 Then list.RemoveAt(index)
Next
' 結果を表示
For Each val As Integer In list
Console.Write("{0}, ", val)
Next
Console.WriteLine()
End Sub
End Class
4,
別のコレクションに結果を格納する
もう一つは、元のコレクションには変更を加えず、別のコレクションを用意してそこに結果を格納する方法です。 この方法の場合、元のコレクションには変更を加えないため、foreach文/For Eachステートメントでの列挙中でも例外はスローされません。 先の例ではリストから奇数の要素を削除するようにしていましたが、次のコードでは奇数以外の要素(つまり偶数)を別のリストに格納することで同等の結果を得られるようにしています。
using System;
using System.Collections.Generic;
class Sample {
static void Main()
{
List<int> list = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7});
// Listから奇数の要素を削除したい
List<int> result = new List<int>(); // 結果を格納するList
foreach (int val in list) {
if (val % 2 == 0)
result.Add(val); // 偶数の場合、結果のListに追加
}
// 結果を表示
foreach (int val in result) {
Console.Write("{0}, ", val);
}
Console.WriteLine();
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7})
' Listから奇数の要素を削除したい
Dim result As New List(Of Integer) ' 結果を格納するList
For Each val As Integer In list
If val Mod 2 = 0 Then result.Add(val) ' 偶数の場合、結果のListに追加
Next
' 結果を表示
For Each val As Integer In result
Console.Write("{0}, ", val)
Next
Console.WriteLine()
End Sub
End Class
2, 4, 6,
Whereメソッドを使用する
列挙中にコレクションを変更するような操作は、多くの場合その目的がコレクションからの特定要素の抽出、フィルタリングであると思われます。 このような目的には、LINQのWhereメソッドを用いることができます。
先の例における「奇数の要素を削除したい」というのは、「偶数の要素だけを抽出したい」ということになるため、Whereメソッドを使ってその条件に該当する要素だけを抽出します。
using System;
using System.Collections.Generic;
using System.Linq;
class Sample {
static void Main()
{
List<int> list = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7});
// Listから奇数の要素を削除したい
// →Whereメソッドで偶数の要素だけを抽出したListを作成する
List<int> result = list.Where(e => e % 2 == 0).ToList();
// 結果を表示
foreach (int val in result) {
Console.Write("{0}, ", val);
}
Console.WriteLine();
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7})
' Listから奇数の要素を削除したい
' →Whereメソッドで偶数の要素だけを抽出したListを作成する
Dim result As List(Of Integer) = list.Where(Function(e) e Mod 2 = 0).ToList()
' 結果を表示
For Each val As Integer In result
Console.Write("{0}, ", val)
Next
Console.WriteLine()
End Sub
End Class
2, 4, 6,
目的がフィルタリング以外の場面でも、本当にそのコレクションを変更するような操作が必要なのか、列挙と変更の目的を再検証することにより列挙中のコレクションの変更を回避できる場合があります。
RemoveAllメソッドを使用する
条件に合致する要素を削除することが目的である場合は、List.RemoveAllメソッドを用いることができます。 このメソッドでは、条件をデリゲートの形で指定し、その条件に合致するすべての要素を削除します。
using System;
using System.Collections.Generic;
using System.Linq;
class Sample {
static void Main()
{
List<int> list = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7});
// RemoveAllメソッドを使ってListから奇数の要素を削除する
list.RemoveAll(e => e % 2 != 0);
// 結果を表示
foreach (int val in list) {
Console.Write("{0}, ", val);
}
Console.WriteLine();
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7})
' RemoveAllメソッドを使ってListから奇数の要素を削除する
list.RemoveAll(Function(e) e Mod 2 <> 0)
' 結果を表示
For Each val As Integer In list
Console.Write("{0}, ", val)
Next
Console.WriteLine()
End Sub
End Class
2, 4, 6,
RemoveAllメソッドにおける条件の記述方法などについてはジェネリックコレクション(1) List §.述語(Predicate)を用いた検索を参照してください。
List.ForEachメソッドとコレクションの変更
列挙するコレクションがListクラスの場合は、ForEachメソッドを使って列挙する事が出来ます。 しかし、このメソッドでの列挙中に変更を加える操作はサポートされないため、列挙中のInvalidOperationExceptionを回避する目的でList.ForEachメソッドを使用することはできません。
.NET Framework 4.0以前の場合は、ForEachメソッドでの列挙中に変更を加えてもInvalidOperationExceptionはスローされませんが、結果は未定義となります。 .NET Framework 4.5以降の場合は、列挙中に変更を加えるとforeach文の場合と同様にInvalidOperationExceptionがスローされます。
次の例では、ForEachメソッドでの列挙中に要素の削除を行っていますが、結果は意図した通りにはなりません。
using System;
using System.Collections.Generic;
class Sample {
static void Main()
{
List<int> list = new List<int>(new int[] {2, 4, 5, 6, 1, 3, 7});
// Listから奇数の要素を削除したい
list.ForEach(delegate(int val) {
if (val % 2 != 0)
list.Remove(val); // 奇数の場合、要素を削除
});
// 結果を表示
foreach (int val in list) {
Console.Write("{0}, ", val);
}
Console.WriteLine();
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim list As New List(Of Integer)(New Integer() {2, 4, 5, 6, 1, 3, 7})
' Listから奇数の要素を削除したい
list.ForEach(Sub(val)
If val Mod 2 <> 0 Then list.Remove(val) ' 奇数の場合、要素を削除
End Sub)
' 結果を表示
For Each val As Integer In list
Console.Write("{0}, ", val)
Next
Console.WriteLine()
End Sub
End Class
2, 4, 6, 3,
ForEachメソッドの使い方についてはジェネリックコレクション(1) Listで解説しています。