Как известно, если во время работы foreach коллекция, которую он перебирает будет изменена (в другом потоке, например), то произойдёт исключение.
Но как устроен этот механизм исключений? Кто ответственен за обнаружение изменения
Должен ли я при разработке своих коллекций принимать какие-то меры для поддержки этого поведения или всё всегда будет заводиться из коробки?
Ответы
Ответ 1
Цикл foreach эквивалентен приблизительно следующему коду:
int[] a = { 1, 2, 3, 4 };
var enumerator = ((IEnumerable)a).GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
// ... тело цикла ...
}
Таким образом, все зависит от реализации метода IEnumerable.GetEnumerator()
конкретной коллекции.
Для большинства встроенных в .Net коллекций этот метод возвращает класс, которы
и следит за приватным свойством version коллекций. Которое, в свою очередь увеличивается на 1 каждый раз, когда коллекция меняется.
За исключением одного бага, о котором я уже писал в майкрософт, но толку это не поимело. Метод List.Sort(Comparison comparison) не вызывает инкремент версии.
Ответ 2
Должен ли я при разработке своих коллекций принимать какие-то меры для
поддержки этого поведения или всё всегда будет заводиться из коробки?
А вот это зависит от того, что у вас за коллекции.
Коллекция, всегда возвращающая рандомные элементы - точно не должна об этом задумываться.
Коллекция, работающая с сетью вполне может догружать данные на ходу - аналогично, я бы не падал изза изменения содержимого.
Опирайтесь на предметную логику, а не на массовость поведения.
Ответ 3
Из декомпилированного кода класса List:
public class List : ... {
...
private int _version;
...
public void ForEach(Action action) {
if (action == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action);
int version = this._version;
for (int index = 0; index < this._size && version == this._version; ++index)
action(this._items[index]);
if (version == this._version)
return;
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
}
public List.Enumerator GetEnumerator() {
return new List.Enumerator(this);
}
...
public struct Enumerator : ... {
...
private int _version;
...
internal Enumerator(List list) {
...
this._version = list._version;
...
}
...
public bool MoveNext() {
List list = this._list;
if (this._version != list._version || (uint) this._index >= (uint) list._size)
return this.MoveNextRare();
this._current = list._items[this._index];
++this._index;
return true;
}
private bool MoveNextRare() {
if (this._version != this._list._version)
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
this._index = this._list._size + 1;
this._current = default (T);
return false;
}
...
}
}
Как видно из кода, List хранит номер своей версии, и при создании Enumeratorа это
номер версии записывает в его поле. Тот проверяет это поле в методе MoveNext на соответствие таковому у Listа, и при несоответствии кидает исключение:
internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion() {
throw new InvalidOperationException(
"Collection was modified; enumeration operation may not execute."
);
}
Аналогичная проверка производится в методе ForEach.
Номер версии инкрементируется каждый раз, когда у Listа вызывается метод, изменяющий его содержимое, а именно:
Запись по индексу;
Добавление элемента(ов) или коллекции;
Очистка;
Вставка элемента(ов);
Удаление элемента(ов);
Переворачивание списка;
Сортировка.
Если хранимый тип - ссылочный, то его изменение не считается изменением коллекции и не меняет версию. То есть list[i] = something меняет, а list[i].Update() - нет.
И таки да, если во время перечисления списка провести над списком 232 операций д
того, как перечислитель попытается перейти к следующему элементу, ошибки не выкинется. Запустим такой вот веселый код (выполнение занимает меньше минуты, и можно смотреть прогресс от 0 до 65536):
var list = new List {1};
Console.WriteLine (list [0]);
Console.ReadKey (true);
foreach (uint i in list) {
for (uint j = 1; j != 0; ++j) {
list [0] = j;
if ((j & 65535) == 0) Console.WriteLine (j >> 16);
}
list [0] = 0;
Console.WriteLine ("все еще не упало");
}
Console.WriteLine (list [0]);
Вывод:
1
<здесь жмем любую клавишу, чтобы не потерять 1 в этих логах прогресса эксперимента>
0
1
2
...
65534
65535
все еще не упало
0
Коллекция изменилась, а программа этого не заметила!
Если изменить число изменений, например, удалив цикл for, то мы увидим следующее:
1
<жмем любую клавишу>
все еще не упало
Unhandled Exception:
System.InvalidOperationException: Collection was modified; enumeration operatio
may not execute.
Комментариев нет:
Отправить комментарий