List, Dictionary, Queueなどのコレクションでは、foreach文/For Eachステートメントでの列挙中に要素を追加したり削除したりする操作を行うことはできません。
このような操作を行おうとすると「列挙操作は実行されない可能性があります」というメッセージとともに例外InvalidOperationExceptionがスローされます。 これは、追加・削除を行うことで列挙中のコレクションの列挙子が指す要素の位置が変わり(列挙子が無効となる)、正しい列挙操作を継続できなくなるためです。
列挙中にInvalidOperationExceptionがスローされる例
例として、Listに格納された値のうち、奇数のみを削除するために次のようなコードを書いたとします。 このコードはInvalidOperationExceptionをスローします。
これを回避するには、foreachによる列挙中はコレクションを変更しないようにするしかありません。 逆に言えば、foreachでの列挙中に列挙中のコレクションを変更しなければよいため、次のような代替手段を使うことができます。
代替方法
for文を使う
一つの代替方法として、for文などを使いインデックスを指定してコレクションを列挙・変更する方法があります。 先の例を、for文/Do Whileステートメントを使って書き換えると次のようになります。
この方法では、コレクションを変更することにより列挙中の要素のインデックスが変わる場合がある点に注意が必要です。 追加や削除により要素のインデックスが変化することを考慮しながらループを行う必要があります。 上記の例で言えば、{1, 3, 4, 5}
のようにList内に奇数の連続があると正しく動作しないという問題があります。
削除を行うと削除した要素以降のインデックスが変わることから、上記の例ではコレクションの末尾側から削除するように処理することで問題を回避できます。
別のコレクションに結果を格納する
もう一つは、元のコレクションには変更を加えず、別のコレクションを用意してそこに結果を格納する方法です。 この方法の場合、元のコレクションには変更を加えないため、foreach文/For Eachステートメントでの列挙中でも例外はスローされません。 先の例ではリストから奇数の要素を削除するようにしていましたが、次のコードでは奇数以外の要素(つまり偶数)を別のリストに格納することで同等の結果を得られるようにしています。
Whereメソッドを使用する
列挙中にコレクションを変更するような操作は、多くの場合その目的がコレクションからの特定要素の抽出、フィルタリングであると思われます。 このような目的には、LINQのWhereメソッドを用いることができます。
先の例における「奇数の要素を削除したい」というのは、「偶数の要素だけを抽出したい」ということになるため、Whereメソッドを使ってその条件に該当する要素だけを抽出します。
目的がフィルタリング以外の場面でも、本当にそのコレクションを変更するような操作が必要なのか、列挙と変更の目的を再検証することにより列挙中のコレクションの変更を回避できる場合があります。
RemoveAllメソッドを使用する
条件に合致する要素を削除することが目的である場合は、List.RemoveAllメソッドを用いることができます。 このメソッドでは、条件をデリゲートの形で指定し、その条件に合致するすべての要素を削除します。
RemoveAllメソッドにおける条件の記述方法などについてはジェネリックコレクション(1) List §.述語(Predicate)を用いた検索を参照してください。
List.ForEachメソッドとコレクションの変更
列挙するコレクションがListクラスの場合は、ForEachメソッドを使って列挙する事が出来ます。 しかし、このメソッドでの列挙中に変更を加える操作はサポートされないため、列挙中のInvalidOperationExceptionを回避する目的でList.ForEachメソッドを使用することはできません。
.NET Framework 4.0以前の場合は、ForEachメソッドでの列挙中に変更を加えてもInvalidOperationExceptionはスローされませんが、結果は未定義となります。 .NET Framework 4.5以降の場合は、列挙中に変更を加えるとforeach文の場合と同様にInvalidOperationExceptionがスローされます。
次の例では、ForEachメソッドでの列挙中に要素の削除を行っていますが、結果は意図した通りにはなりません。
ForEachメソッドの使い方についてはジェネリックコレクション(1) Listで解説しています。