Чому цей код кидає "Колекція була змінена", але коли я повторюю щось перед цим, він не робить?


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 перевіряє, чи була збірка змінена на початку кожного циклу .... тому додавання та видалення елемента в кожному циклі не призводить до помилок.
Каз

6
Можливо, ви змогли відповісти на це питання самостійно, переглянувши довідкове джерело та побачивши, як працює виявлення змін. Не всі знають, що довідкове джерело навіть існує, просто поширюючи слово :)
Крістофер Керренс

2
Лише з цікавості: чи було це питання у реальному світі?
ken2k

Відповіді:


119

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

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

(Це деталі реалізації, а не конкретна поведінка - і це деталізація, яку можна спостерігати як помилку в дуже рідкісному випадку. Було б дуже незвично бачити, що це викликає проблеми в реальній програмі.)


5
Тільки для довідки: відповідний вихідний код , зверніть увагу, що _versionполе є int.
Лукас Тшесневський

1
Так, він налаштований саме так, що після завершення циклу _version має значення -2 .... потім додавання 6 і 7 ставить його до 0, завдяки чому список виглядає як немодифікований.
Каз

4
Я не впевнений, що це слід назвати "детальною інформацією про реалізацію", оскільки є побічний ефект цього рішення про реалізацію, який, навіть якщо навряд чи станеться, справжній. Спекуляція (або, принаймні, док) говорить, що вона повинна кинути InvalidOperationException, що насправді не завжди відповідає дійсності. Звичайно, це залежить від визначення "деталізації реалізації".
ken2k

3
Джон Скіт, ви дизайнер мови програмування? (Не знайшли нічого, пов’язаного з Google) Трохи цікаво, чому ви теж маєте ці знання. Це питання було трохи дражнити, щоб побачити "потужність" Стек Переповнення.
LyingOnTheSky

6
@LyingOnTheSky: Ні, хоча мені подобається грати, будучи дизайнером мови з точки зору дотримання та критики мови C #. Я також перебуваю в технічній групі ECMA-334 для стандартизації C # 5 ... тому я можу вибирати отвори, але не виконувати реальну мовну розробку :)
Jon Skeet
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.