Почему этот код выдает сообщение «Коллекция была изменена», но когда я что-то повторяю перед этим, это не так?

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

Если вы закомментируете внутренний forцикл, он выдаст, очевидно, потому что мы внесли изменения в коллекцию.

Теперь, если вы его раскомментируете, почему этот цикл позволяет нам добавлять эти два элемента? Требуется некоторое время, чтобы запустить его, например, полминуты (на процессоре Pentium), но он не срабатывает, и самое забавное, что он выводит:

Образ

Это было немного ожидаемо, но это указывает на то, что мы можем изменить, и это фактически меняет коллекцию. Есть идеи, почему возникает такое поведение?

Лежащий на небе
источник
2
Это интересно. Я мог бы воспроизвести поведение, но не если бы я изменил внутренний цикл с Int.MaxValue на значение, например 100
Стив
Как долго ты ждал? Завершение int.MaxValueитераций занимает довольно много времени ...
Джон Скит,
1
Я считаю, что foreach проверяет, была ли изменена коллекция в начале каждого цикла ... поэтому добавление, а затем удаление элемента в каждом цикле не вызывает никаких ошибок.
Kaz
6
Вы могли бы сами ответить на этот вопрос, посмотрев на справочный источник и увидев, как работает обнаружение изменений. Не все знают, что справочный источник вообще существует, просто распространяю информацию :)
Кристофер Карренс
2
Просто из любопытства: была ли у вас эта проблема в реальном куске кода?
ken2k 03

Ответы:

119

Проблема в том, что способ List<T>обнаружения модификаций заключается в сохранении поля версии типа int, увеличивая его при каждой модификации. Следовательно, если вы внесли ровно несколько из 2 32 изменений в список между итерациями, это сделает эти изменения невидимыми с точки зрения обнаружения. (Он будет переполняться от int.MaxValueдо int.MinValueи в конечном итоге вернется к своему исходному значению.)

Если вы измените что-либо в своем коде - добавьте 1 или 3 значения, а не 2, или уменьшите количество итераций вашего внутреннего цикла на 1, тогда он выдаст исключение, как и ожидалось.

(Это деталь реализации, а не конкретное поведение - и это деталь реализации, которая может быть замечена как ошибка в очень редких случаях. Однако было бы очень необычно увидеть, как это вызывает проблему в реальной программе.)

Джон Скит
источник
5
Только для справки: соответствующий исходный код , обратите внимание, что это _versionполе int.
Лукас Тшесневски
1
Ага, он настроен так, что после завершения цикла for _version имеет значение -2 .... затем добавление 6 и 7 приводит его к 0, делая список неизменным.
Kaz
4
Я не уверен, что это следует называть «деталью реализации», потому что это решение о реализации имеет побочный эффект, который, даже если маловероятный, является реальным. В спецификации (или, по крайней мере, в документе) говорится, что он должен выбросить InvalidOperationException, что на самом деле не всегда верно. Конечно, это зависит от определения «детали реализации».
ken2k 03
3
Джон Скит, вы дизайнер языков программирования? (Не нашел ничего похожего в Google) Немного любопытно, почему у вас тоже есть эти знания. Этот вопрос был своего рода подразниванием, чтобы увидеть «мощь» Stack Overflow.
LyingOnTheSky
6
@LyingOnTheSky: Нет, хотя мне нравится играть в языкового дизайнера в том, что касается следования языку C # и его критики. Я также в технической группе ECMA-334 по стандартизации C # 5 ... так что я могу выискивать дыры, но не заниматься реальным дизайном языка :)
Джон Скит