Dlaczego ten kod zgłasza „Kolekcja została zmodyfikowana”, ale kiedy wykonuję iterację przed nim, tak nie jest?

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

Jeśli skomentujesz wewnętrzną forpętlę, zostanie ona wyrzucona , jest to oczywiście spowodowane zmianami w kolekcji.

Jeśli teraz odkomentujesz to, dlaczego ta pętla pozwala nam dodać te dwa elementy? Trwa trochę czasu, aby uruchomić go jak pół minuty (na procesorze Pentium), ale nie rzuca się, a zabawne jest to, że wyświetla:

Wizerunek

Było to trochę oczekiwane, ale wskazuje, że możemy się zmienić i to faktycznie zmienia kolekcję. Jakieś pomysły, dlaczego takie zachowanie występuje?

LyingOnTheSky
źródło
2
To interesujące. Mogę odtworzyć to zachowanie, ale nie, jeśli zmienię wewnętrzną pętlę z Int.MaxValue na wartość taką jak 100
Steve
Jak długo czekałeś Zakończenie int.MaxValueiteracji zajmuje trochę czasu ...
Jon Skeet
1
Uważam, że foreach sprawdza, czy kolekcja została zmodyfikowana na początku każdej pętli .... więc dodanie, a następnie usunięcie elementu w każdej pętli nie powoduje żadnych błędów.
Kaz
6
Być może byłbyś w stanie samodzielnie odpowiedzieć na to pytanie, patrząc na źródło odniesienia i sprawdzając, jak działa wykrywanie zmian. Nie wszyscy wiedzą, że źródło odniesienia w ogóle istnieje, po prostu je rozpowszechnia :)
Christopher Currens
2
Tak z ciekawości: czy miałeś ten problem w prawdziwym fragmencie kodu?
ken2k

Odpowiedzi:

119

Problem polega na tym, że sposobem List<T>wykrywania modyfikacji jest zachowanie pola wersji, typu inti zwiększanie go przy każdej modyfikacji. Dlatego, jeśli dokonałeś dokładnie wielokrotności 2 32 modyfikacji listy między iteracjami, spowoduje to, że te modyfikacje będą niewidoczne, jeśli chodzi o wykrywanie. (Przepełni się z int.MaxValuedo int.MinValuei ostatecznie powróci do swojej wartości początkowej).

Jeśli zmienisz prawie wszystko w swoim kodzie - dodasz 1 lub 3 wartości zamiast 2 lub zmniejsz liczbę iteracji wewnętrznej pętli o 1, to zgłosi wyjątek zgodnie z oczekiwaniami.

(Jest to raczej szczegół implementacji niż określone zachowanie - i jest to szczegół implementacji, który można zaobserwować jako błąd w bardzo rzadkich przypadkach. Jednak byłoby bardzo nietypowe, gdyby spowodował problem w prawdziwym programie).

Jon Skeet
źródło
5
Tylko w celach informacyjnych: odpowiedni kod źródłowy , zwróć uwagę, że _versionpole to int.
Lucas Trzesniewski
1
Tak, jest tak skonfigurowany, że po zakończeniu pętli for _version ma wartość -2 .... a następnie dodanie 6 i 7 ustawia ją na 0, dzięki czemu lista wygląda tak, jakby była niezmodyfikowana.
Kaz
4
Nie jestem pewien, czy należy to nazwać „szczegółem implementacji”, ponieważ istnieje efekt uboczny tej decyzji o wdrożeniu, który nawet jeśli jest mało prawdopodobny, jest prawdziwy. Specyfikacja (a przynajmniej dokumentacja) mówi, że powinna wyrzucić InvalidOperationException, co w rzeczywistości nie zawsze jest prawdą. Oczywiście zależy to od definicji „szczegółów implementacji”.
ken2k
3
Jon Skeet, czy jesteś projektantem języków programowania? (Nie znalazłem nic związanego z Google) Trochę ciekawy, dlaczego ty też masz tę wiedzę. To pytanie było trochę drażniące, aby zobaczyć „moc” Stack Overflow.
LyingOnTheSky
6
@LyingOnTheSky: Nie, chociaż lubię bawić się projektantem języka, jeśli chodzi o śledzenie i krytykowanie języka C #. Jestem również na grupie technicznego ECMA-334 do standaryzacji C # 5 ... więc dostać się do dziury, ale nie zrobić prace projektowe prawdziwy język :)
Jon Skeet