Warum dieser Code "Sammlung wurde geändert" auslöst, aber wenn ich etwas davor iteriere, ist dies nicht der Fall?

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

Wenn Sie die innere forSchleife auskommentieren, wird sie ausgelöst, weil wir offensichtlich Änderungen an der Sammlung vorgenommen haben.

Wenn Sie es nun auskommentieren, warum können wir in dieser Schleife diese beiden Elemente hinzufügen? Es dauert eine Weile, bis es wie eine halbe Minute ausgeführt wird (auf Pentium-CPU), aber es wirft nicht, und das Lustige ist, dass es Folgendes ausgibt:

Bild

Es wurde ein bisschen erwartet, aber es zeigt an, dass wir uns ändern können und es ändert tatsächlich die Sammlung. Irgendwelche Ideen, warum dieses Verhalten auftritt?

LyingOnTheSky
quelle
2
Das ist interessant. Ich könnte das Verhalten reproduzieren, aber nicht, wenn ich die interne Schleife von Int.MaxValue auf einen Wert wie 100 ändere
Steve
Wie lange hast du gewartet? Es dauert eine ganze Weile, bis die int.MaxValueIterationen abgeschlossen sind ...
Jon Skeet
1
Ich glaube, die foreach prüft, ob die Sammlung am Anfang jeder Schleife geändert wurde. Das Hinzufügen und Entfernen des Elements in jeder Schleife wirft also keine Fehler auf.
Kaz
6
Möglicherweise konnten Sie diese Frage selbst beantworten, indem Sie sich die Referenzquelle ansehen und sehen, wie die Änderungserkennung funktioniert. Nicht jeder weiß, dass die Referenzquelle überhaupt existiert, nur um das Wort zu verbreiten :)
Christopher Currens
2
Nur aus Neugier: Hatten Sie dieses Problem in einem realen Code?
Ken2k

Antworten:

119

Das Problem besteht darin, dass List<T>Änderungen erkannt werden, indem ein Versionsfeld vom Typ intbeibehalten und bei jeder Änderung erhöht wird. Wenn Sie also zwischen den Iterationen genau ein Vielfaches von 2 32 Änderungen an der Liste vorgenommen haben, werden diese Änderungen für die Erkennung unsichtbar. (Es wird von int.MaxValuebis überlaufen int.MinValueund schließlich zu seinem ursprünglichen Wert zurückkehren.)

Wenn Sie so ziemlich alles an Ihrem Code ändern - fügen Sie 1 oder 3 Werte anstelle von 2 hinzu oder verringern Sie die Anzahl der Iterationen Ihrer inneren Schleife um 1, wird wie erwartet eine Ausnahme ausgelöst.

(Dies ist eher ein Implementierungsdetail als ein angegebenes Verhalten - und es ist ein Implementierungsdetail, das in einem sehr seltenen Fall als Fehler angesehen werden kann. Es wäre jedoch sehr ungewöhnlich, dass es in einem realen Programm ein Problem verursacht.)

Jon Skeet
quelle
5
Nur als Referenz: Relevanter Quellcode , beachten Sie, dass das _versionFeld ein ist int.
Lucas Trzesniewski
1
Ja, es ist genau richtig eingerichtet, so dass _version nach Abschluss der for-Schleife den Wert -2 hat. Wenn Sie dann 6 und 7 hinzufügen, wird es auf 0 gesetzt, sodass die Liste so aussieht, als wäre sie unverändert.
Kaz
4
Ich bin mir nicht sicher, ob dies als "Implementierungsdetail" bezeichnet werden sollte, da diese Implementierungsentscheidung einen Nebeneffekt hat, der real ist, auch wenn dies unwahrscheinlich ist. Die Spezifikation (oder zumindest das Dokument) sagt, es sollte eine werfen InvalidOperationException, was eigentlich nicht immer wahr ist. Dies hängt natürlich von der Definition des "Implementierungsdetails" ab.
Ken2k
3
Jon Skeet, sind Sie Programmiersprachen-Designer? (Ich habe bei Google nichts Ähnliches gefunden.) Ein bisschen neugierig, warum Sie dieses Wissen auch haben. Diese Frage war ein bisschen necken, um die "Kraft" von Stack Overflow zu sehen.
LyingOnTheSky
6
@LyingOnTheSky: Nein, obwohl ich gerne als Sprachdesigner spiele, wenn es darum geht, der C # -Sprache zu folgen und sie zu kritisieren. Ich bin auch in der technischen Gruppe ECMA-334 für die Standardisierung von C # 5 ... also kann ich Löcher auswählen, aber nicht die eigentliche Sprachdesignarbeit machen :)
Jon Skeet