Что изменилось в .net 5, что заставляет его не бросать при изменении значений словаря в foreach

В .NET ‹5 и .NET Core 3.1 следующий код

var d = new Dictionary<string, int> { { "a", 0 }, { "b", 0 }, { "c", 0 } };
foreach (var k in d.Keys) 
{
   d[k]+=1;
}

бросает

System.InvalidOperationException: коллекция была изменена; операция перечисления может не выполняться.

При таргетинге на .NET 5 фрагмент больше не выдает.

Что изменилось?

Мне не удалось найти ответ в Критические изменения в .NET 5 и Повышение производительности в .NET 5.

Это как-то связано с ref readonly T?


person tymtam    schedule 04.04.2021    source источник
comment
Я думаю, мы сможем найти ответ в исходном коде на Dictionary<, >, начав чтение с set средства доступа индексатора public TValue this[TKey key] { /* ... */ }. Должно существовать личное поле, которое отслеживает, была ли изменена коллекция или нет. Если я назначаю с помощью установщика d[k] = tmp; и k ранее был в Dictionary<,>, считается ли это модификацией? Должно быть, они это изменили.   -  person Jeppe Stig Nielsen    schedule 04.04.2021
comment
В старой версии делают entries[i].value = value; version++; return; в ветке где у нас обновление. version++ гарантирует, что счетчик взорвется с исключением на следующем MoveNext() в счетчике.   -  person Jeppe Stig Nielsen    schedule 04.04.2021
comment
(Они также разрешают это с помощью коллекции значений, например foreach (var v in d.Values) { Console.WriteLine("Top " + v + " " + d["c"]); d["c"] += 1; Console.WriteLine("Bottom " + v + " " + d["c"]); }. Так что я думаю, что есть только одно поле version для ключей и значений. Где я могу увидеть новый исходный код C # для public class Dictionary<TKey, TValue>?)   -  person Jeppe Stig Nielsen    schedule 04.04.2021


Ответы (1)


В исходный код Dictionary<TKey, TValue> было внесено изменение, позволяющее обновлять существующие ключи во время перечисления. Его совершил 9 апреля 2020 года Стивен Туб. Эту фиксацию можно найти PR № 34667.

PR называется Разрешить перезапись словаря во время перечисления и отмечает, что он устраняет проблему № 34606. Рассмотрите возможность удаления _version++ от перезаписи в Dictionary<TKey, TValue>. Текст этого номера, открытого г-ном Тубом, выглядит следующим образом:

Ранее мы удалили _version++ при удалении из словаря. Мы должны подумать об этом и при простой перезаписи значения для существующего ключа в словаре. Это позволит включить циклы обновления, которые изменяют значение в словаре, без необходимости прибегать к запутанным и более дорогостоящим методам.

В комментариях к этому вопросу спрашивается:

Какая польза от этого?

На что Стивен Туб ответил:

Как указано в исходном посте, тонкие шаблоны, которые в настоящее время бросают сегодня, начнут работать правильно, например

foreach (KeyValuePair<string, int> pair in dict) dict[pair.Key] = pair.Value + 1;

Если вы посмотрите на Dictionary<, > исходный код, вы можете видеть, что поле _version (которое используется для обнаружения изменений) теперь обновляется только при определенных условиях, а нет при изменении существующего ключа.

Особый интерес представляет область _ 9_ (который вызывается индексатором, см. Ниже) и его третий параметр типа InsertionBehavior. Если это значение равно InsertionBehavior.OverwriteExisting, поле управления версиями не обновляется для существующего ключа.

Например, см. Этот раздел кода из обновленного TryInsert:

if (behavior == InsertionBehavior.OverwriteExisting)
{ 
    entries[i].value = value;
    return true;
}

До изменения этот раздел выглядел следующим образом (мой комментарий кода):

if (behavior == InsertionBehavior.OverwriteExisting)
{ 
    entries[i].value = value;
    _version++; // <-----
    return true;
}

Обратите внимание, что приращение поля _version было удалено, что позволяет вносить изменения во время перечисления.

Для полноты, установщик индексатора выглядит так. Он не был изменен этим изменением, но обратите внимание на третий параметр, который влияет на приведенное выше поведение:

set 
{
    bool modified = TryInsert(key, value, InsertionBehavior.OverwriteExisting);
    Debug.Assert(modified);
} 

Remove из словаря больше не влияет на перечисление. Это, однако, существует с netcore 3.0 и соответствующим образом вызывается в документации Remove:

Только .NET Core 3.0+: этот изменяющий метод можно безопасно вызывать без недействительности активных перечислителей в экземпляре Dictionary<TKey,TValue>. Это не подразумевает потокобезопасность.

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

person pinkfloydx33    schedule 04.04.2021
comment
Хорошая находка! Изменение в файле Dictionary.cs (ссылка на фиксацию в вашем ответе) на самом деле в точности такое, как я ожидал. Интересно, есть ли смысл внести изменения и в List<>? А именно, когда вы выполняете list[index] = newvalue; (то есть средство доступа индексатора set), это только перезаписывает значение в уже существующем слоте. Таким образом, это связано с моим ответом в другом потоке, где объясняется этот аспект List<>. - person Jeppe Stig Nielsen; 04.04.2021
comment
@JeppeStigNielsen, спасибо. Много копался в виноватых git, прежде чем я понял, что могу просто посмотреть историю файлов lol - person pinkfloydx33; 04.04.2021
comment
На сегодняшний день, чтобы реализовать изменение в List<>, о котором я говорю, все, что нужно, - это удалить строка 162 в файле List.cs. Таким образом, любой, кто видит это, может сделать запрос на перенос и испортить мой ответ из другого потока (предыдущий мой комментарий). - person Jeppe Stig Nielsen; 04.04.2021