Mengapa List <T> .ForEach mengizinkan daftarnya diubah?

90

Jika saya menggunakan:

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

bagian Adddalam foreachmelempar sebuah InvalidOperationException (Koleksi telah dimodifikasi; operasi pencacahan tidak dapat dijalankan), yang saya anggap logis, karena kita menarik permadani dari bawah kaki kita.

Namun, jika saya menggunakan:

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

itu segera menembak dirinya sendiri di kaki dengan perulangan sampai melempar OutOfMemoryException.

Ini adalah kejutan bagi saya, karena saya selalu berpikir bahwa List.ForEach hanyalah pembungkus untuk foreachatau untuk for.
Adakah yang punya penjelasan tentang bagaimana dan mengapa perilaku ini?

(Terinspirasi oleh perulangan ForEach untuk Daftar Generik yang berulang tanpa henti )

SWeko
sumber
7
Saya setuju. Ini - meragukan. Saya ingin Anda mempostingnya di microsoft connect dan meminta klarifikasi.
TomTom
4
"Ini merupakan kejutan bagi saya, karena saya selalu berpikir bahwa List.ForEach hanyalah pembungkus untuk foreachatau untuk for." Itu masih bisa digunakan for. Anda dapat melakukan tindakan yang sama dalam satu forputaran dan menghasilkan OutOfMemoryException yang sama sebagai hasilnya.
Anthony Pegram
Ini berdasarkan pertanyaan saya: stackoverflow.com/q/9311272/132239 , terima kasih SWeko karena telah membahas detailnya
Kasrak

Jawaban:

68

Itu karena ForEachmetode ini tidak menggunakan pencacah, itu formengulang melalui item dengan perulangan:

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]);
    }
}

(kode diperoleh dengan JustDecompile)

Karena enumerator tidak digunakan, enumerator tidak pernah memeriksa apakah daftar telah berubah, dan kondisi akhir dari forperulangan tidak pernah tercapai karena _sizebertambah pada setiap iterasi.

Thomas Levesque
sumber
Ya, tapi bagaimana cara _sizemenghitungnya? Jika itu hanya dihitung sebelumnya maka jika harus dijalankan sekali untuk contoh saya. Ini jelas disegarkan entah bagaimana.
SWeko
7
Ini di-refresh di Add method -> this._items [this._size ++] = item;
Fabio
1
@SWeko, itu tidak dihitung, itu diperbarui setiap kali item ditambahkan atau dihapus.
Thomas Levesque
1
Ada _versionvariabel privat List<T>yang dapat mendeteksi jenis skenario ini, karena diperbarui pada operasi yang mengubah daftar itu sendiri.
SWeko
Anda dapat menghindari pengecualian dengan terlebih dahulu mendapatkan ukurannya (int theSize = this._size), lalu menggunakannya dalam for loop?
Lazlow
14

List<T>.ForEachdiimplementasikan melalui fordalam, sehingga tidak menggunakan pencacah dan memungkinkan untuk memodifikasi koleksi.

Alexey Raga
sumber
6

Karena ForEach yang dilampirkan ke kelas Daftar secara internal menggunakan perulangan for yang langsung dilampirkan ke anggota internalnya - yang dapat Anda lihat dengan mengunduh kode sumber untuk kerangka .NET.

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

Dimana foreach loop adalah yang pertama dan terutama pengoptimalan compiler tetapi juga harus beroperasi terhadap koleksi sebagai pengamat - jadi jika koleksi diubah, pengecualian akan muncul.

Mike Perrenoud
sumber
Dan untuk menjawab komentar di kiriman @Thomas tentang cara menyegarkannya - anggota internal disegarkan ketika add dipanggil sehingga mereka dapat mengikuti perubahan. Jika Anda melakukan penyisipan, pada indeks yang kurang dari yang sekarang, Anda tidak akan pernah mengoperasikan item itu karena itu sudah diulang melewati item itu. Tetapi karena Anda menambahkan pada akhirnya, itu berhasil.
Mike Perrenoud
1
Ya, mengubah Addbaris strings.Insert(0, s + "!")hanya dengan mencetak 'sampel'. Aneh bahwa ini tidak disebutkan sama sekali dalam dokumentasi.
SWeko
Yah, saya pikir Microsoft menyadari bahwa hampir tidak mungkin untuk memberikan setiap peringatan yang ada dalam dokumentasi mereka - jadi mereka menyediakan kode sumbernya sekarang. Jujur saja, saya menemukan solusi yang lebih baik tetapi satu-satunya masalah yang saya temukan adalah bahwa produk seperti WF tidak diperbarui secepatnya - 4.x kode sumber WF masih belum tersedia.
Mike Perrenoud
4

Kami tahu tentang masalah ini, itu adalah kekeliruan ketika aslinya ditulis. Sayangnya, kami tidak dapat mengubahnya karena sekarang akan mencegah kode yang sebelumnya berfungsi ini untuk berjalan:

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

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

Kegunaan metode ini sendiri dipertanyakan seperti yang ditunjukkan oleh Eric Lippert , jadi kami tidak menyertakannya untuk .NET untuk aplikasi gaya Metro (yaitu aplikasi Windows 8).

David Kean (Tim BCL)

David Kean
sumber
1
Saya melihat bahwa ini akan menjadi perubahan besar yang buruk, tetapi bagaimanapun juga itu bisa gagal dengan cara yang tidak jelas, dan itu tidak pernah merupakan hal yang baik. Saya tidak dapat melihat skenario di mana menggunakan metode ForEach lebih unggul daripada yang sederhana (atau sebelumnya jika mangling dari daftar asli tidak diperlukan)
SWeko