List <T> .ForEachでリストを変更できるのはなぜですか?


90

私が使用する場合:

var strings = new List<string> { "sample" };
foreach (string s in strings)
{
  Console.WriteLine(s);
  strings.Add(s + "!");
}

Add中には、foreach私たちは私たちの足の下から敷物を引いているので、私は、論理的な検討している、;(列挙操作が実行されない可能性コレクションが変更された)と、InvalidOperationExceptionがスローされます。

ただし、私が使用する場合:

var strings = new List<string> { "sample" };
strings.ForEach(s =>
  {
    Console.WriteLine(s);
    strings.Add(s + "!");
  });

OutOfMemoryExceptionがスローされるまでループすることで、すぐに足を撃ちます。

List.ForEachは単なるforeachまたはのラッパーであるといつも思っていたので、これは私にとって驚きですfor
この動作の方法と理由について誰かが説明していますか?

ジェネリックリストのForEachループによって無限に繰り返されます


7
同意する。これは疑わしいです。それをマイクロソフトコネクトに投稿し、説明を求めます。
TomTom

4
「List.ForEachは単なるかのラッパーのどちらかだといつも思っていたので、これは驚きforeachですfor。」それでも使用できますforforループで同じアクションを実行し、結果として同じOutOfMemoryExceptionを生成できます。
アンソニー

これは私の質問に基づいています:stackoverflow.com/q/9311272/132239、詳細を説明してくれたSWekoに感謝
Kasrak

回答:


68

これは、ForEachメソッドが列挙子を使用しないためであり、アイテムをforループでループします。

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

(JustDecompileで取得したコード)

列挙子は使用されないため、リストが変更されたかどうかを確認することforはなく、ループが_size繰り返しごとに増加するため、ループの終了条件に到達することはありません。


ええ、でもどのように_size計算されますか?事前に計算されている場合は、例では一度だけ実行する必要があります。どういうわけか明らかに更新されています。
SWeko

7
Addメソッドでリフレッシュされます-> this._items [this._size ++] = item;
Fabio

1
@SWeko、これは計算されません。アイテムが追加または削除されるたびに更新されます。
トーマス・レベスク

1
リスト自体を変更する操作で更新されるため、この種のシナリオを検出できる_versionプライベート変数がありList<T>ます。
SWeko

最初にサイズ(int theSize = this._size)を取得し、次にforループ内で使用することにより、例外を回避できますか?
Lazlow、2012

14

List<T>.ForEachfor内部で実装されているため、列挙子を使用せず、コレクションを変更できます。


6

ListクラスにアタッチされているForEachは、内部メンバーに直接アタッチされているforループを内部で使用しているためです。これは、.NETフレームワークのソースコードをダウンロードすることで確認できます。

http://referencesource.microsoft.com/netframework.aspx

foreachループは何よりもまずコンパイラの最適化ですが、オブザーバーとしてのコレクションに対しても動作する必要があるため、コレクションが変更されると例外がスローされます。


そして、@ Thomasの投稿がどのように更新されるかについてのコメントに答えるために、addが呼び出されると内部メンバーが更新されるため、変更に対応できます。現在のインデックスよりも小さいインデックスで挿入を実行する場合、そのアイテムは既に反復されているため、そのアイテムを操作することはありません。しかし、最後に追加しているので機能します。
Mike Perrenoud、2012

1
はい、Add行を変更すると、strings.Insert(0, s + "!")「サンプル」が出力されます。これがドキュメントにまったく記載されていないのは奇妙です。
SWeko

まあ、私はマイクロソフトがドキュメントに存在するすべての警告を提供することはほぼ不可能であることを認識していたと思います-それで彼らは彼らのソースコードを今提供します。私はより良い解決策を正直に見つけますが、私が見つけた唯一の問題は、WFのような製品がそれほど速く更新されないことです-4.x WFソースコードはまだ利用できません。
Mike Perrenoud、2012

4

私たちはこの問題について知っています。最初に書かれたとき、それは見落としでした。残念ながら、以前は機能していたこのコードが実行できなくなるため、変更することはできません。

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

        list.ForEach((item) => 
        { 
            if(item=="Foo") 
                list.Remove(item); 
        });

Eric Lippertが指摘したように、この方法自体の有用性は疑わしいため、Metroスタイルアプリ(Windows 8アプリなど)の.NETには含まれていません。

デビッドキーン(BCLチーム)


1
これは大きな重大な重大な変更になると思いますが、それでも明らかではない方法で失敗する可能性があります。それは決して良いことではありません。私は(元のリストのマングリングが必要とされない場合、またはforeachの)のForEachメソッドを使用するための簡単に優れているシナリオを見ることができない
SWeko
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.