Mengapa kode ini menampilkan 'Collection telah dimodifikasi', tetapi ketika saya mengulang sesuatu sebelumnya, ternyata tidak?

102
var ints = new List< int >( new[ ] {
    1,
    2,
    3,
    4,
    5
} );
var first = true;
foreach( var v in ints ) {
    if ( first ) {
        for ( long i = 0 ; i < int.MaxValue ; ++i ) { //<-- The thing I iterate
            ints.Add( 1 );
            ints.RemoveAt( ints.Count - 1 );
        }
        ints.Add( 6 );
        ints.Add( 7 );
    }
    Console.WriteLine( v );
    first = false;
}

Jika Anda mengomentari forloop dalam , itu akan terlempar, itu jelas karena kami melakukan perubahan pada koleksi.

Sekarang jika Anda menghapus komentar, mengapa loop ini memungkinkan kita menambahkan dua item tersebut? Dibutuhkan beberapa saat untuk menjalankannya seperti setengah menit (Pada Pentium CPU), tetapi tidak membuang, dan lucunya adalah ia mengeluarkan:

Gambar

Itu sedikit diharapkan, tetapi ini menunjukkan bahwa kami dapat mengubah dan itu benar-benar mengubah koleksi. Adakah ide mengapa perilaku ini terjadi?

LyingOnTheSky
sumber
2
Itu menarik. Saya dapat mereproduksi perilakunya, tetapi tidak jika saya mengubah loop internal dari Int.MaxValue menjadi nilai seperti 100
Steve
Berapa lama Anda menunggu? Butuh cukup lama untuk menyelesaikan int.MaxValueiterasi ...
Jon Skeet
1
Saya percaya foreach memeriksa untuk melihat apakah koleksi telah dimodifikasi di awal setiap loop .... jadi menambahkan dan kemudian menghapus item dalam setiap loop tidak menimbulkan kesalahan.
Kaz
6
Anda mungkin dapat menjawab sendiri pertanyaan ini dengan melihat sumber referensi dan melihat cara kerja deteksi perubahan. Tidak semua orang tahu sumber referensi itu ada, hanya menyebarkan berita :)
Christopher Currens
2
Hanya ingin tahu: apakah Anda memiliki masalah ini dalam kode dunia nyata?
ken2k

Jawaban:

119

Masalahnya adalah cara List<T>mendeteksi modifikasi adalah dengan mempertahankan bidang versi, jenis int, menambahkannya pada setiap modifikasi. Oleh karena itu, jika Anda telah membuat persis beberapa beberapa 2 32 modifikasi ke daftar antara iterasi, itu akan membuat mereka modifikasi terlihat sejauh deteksi yang bersangkutan. (Ini akan meluap dari int.MaxValueke int.MinValuedan akhirnya kembali ke nilai awalnya.)

Jika Anda mengubah hampir semua hal tentang kode Anda - tambahkan 1 atau 3 nilai daripada 2, atau turunkan jumlah iterasi loop dalam Anda sebanyak 1, maka itu akan memunculkan pengecualian seperti yang diharapkan.

(Ini adalah detail implementasi daripada perilaku yang ditentukan - dan ini adalah detail implementasi yang dapat diamati sebagai bug dalam kasus yang sangat jarang terjadi. Akan sangat tidak biasa melihatnya menyebabkan masalah dalam program nyata.)

Jon Skeet
sumber
5
Hanya untuk referensi: kode sumber yang relevan , perhatikan bahwa _versionbidangnya adalah int.
Lucas Trzesniewski
1
Ya, ini diatur dengan benar sehingga setelah perulangan for selesai, _version memiliki nilai -2 .... lalu menambahkan 6 dan 7 meletakkannya ke 0, membuat daftar terlihat seperti tidak dimodifikasi.
Kaz
4
Saya tidak yakin ini harus disebut "detail implementasi", karena ada efek samping dari keputusan penerapan itu, yang bahkan jika tidak mungkin terjadi, adalah nyata. Spesifikasi (atau setidaknya doc) mengatakan itu harus melempar InvalidOperationException, yang sebenarnya tidak selalu benar. Tentu saja ini tergantung pada definisi "detail implementasi".
ken2k
3
Jon Skeet, apakah Anda desainer bahasa pemrograman? (Tidak menemukan apa pun yang berhubungan dengan Google) Sedikit penasaran mengapa Anda memiliki pengetahuan ini juga. Pertanyaan ini sedikit menggoda untuk melihat "kekuatan" Stack Overflow.
LyingOnTheSky
6
@LyingOnTheSky: Tidak, meskipun saya suka bermain sebagai desainer bahasa dalam hal mengikuti dan mengkritik bahasa C #. Saya juga berada di grup teknis ECMA-334 untuk standarisasi C # 5 ... jadi saya harus memilih lubang tetapi tidak melakukan pekerjaan desain bahasa yang sebenarnya :)
Jon Skeet