Сайд-ефект внутрішньої реалізації List
Якщо ви робите foreach по деякому List -у, то міняти ітеріруемий лист всередині циклу вкрай не рекомендується, адже це вірний спосіб отримати InvalidOperationException. А тепер загадка: як думаєте, що трапиться з наступним кодом:
Правильна відповідь: цей код чудово відпрацює. На консолі ви побачите:
Розгадка криється у внутрішній реалізації класу List (див. Реалізацію в MS.NET і в Mono 3.10). При ітерірованіі наш List повинен якось стежити, чи не поміняв його хто-небудь всередині черговий ітерації. Для цього використовується приватне поле _version. При будь-якої операції _version збільшується на 1. При створенні Enumerator-для циклу це значення запам'ятовується. а при кожному виклику MoveNext відбувається перевірка. що номер версії не змінився. Якщо хтось міняв елементи колекції, то буде кинутий InvalidOperationException.
Але наведений вище код відмінно відпрацьовує без будь-яких винятків. Як же так? Розгадка проста: для зберігання _version використовується тип int. А що буде, якщо int -змінного збільшити на 1 рівно 2 32 рази? Вона повернеться до свого початкового значення. У прикладі внутрішній цикл (від int.MinValue до int.MaxValue) змінює бідний _version рівно 2 32 -1 раз. А рядок list.Add (3) поповнює лист новим елементом і робить фінальний інкремент _version. який повертає його до початкового значення. В результаті при наступному виклику MoveNext () ніхто не підозрює, що ми щось поміняли. Ідеальний злочин.
Документація нам говорить. що виключення повинно бути кинуто, якщо хтось поміняв колекцію. Так що формально цей приклад ілюструє невелику .NET-багу. Втім, особливо хвилюватися з цього приводу не варто: ймовірність наштовхнутися на подібну проблему реальному житті досить мала. Закладатися на таку поведінку і якось його враховувати теж не варто, тому що згодом воно може змінитися (наприклад, _version зроблять 64-бітовим).