.net 5 中的更改使其在更改 foreach 中的字典值时不会抛出

tym*_*tam 33 c# .net-5

在 .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;
}
Run Code Online (Sandbox Code Playgroud)

投掷

System.InvalidOperationException:集合已修改;枚举操作可能无法执行。

当面向 .NET 5 时,代码段不再抛出。

发生了什么变化?

我未能在Breaking changes in .NET 5Performance Improvements in .NET 5 中找到答案。

是不是和什么有关ref readonly T

pin*_*x33 44

对 的源代码进行了更改,Dictionary<TKey, TValue>以允许在枚举期间更新现有密钥。它由 Stephen Toub 于 2020 年 4 月 9 日提交。可以在此处找到该提交以及相应的PR #34667

PR 的标题是“在枚举期间允许字典覆盖”,并指出它修复了问题 #34606 “考虑_version++从覆盖中删除Dictionary<TKey, TValue>”。Toub 先生开篇的该问题案文如下:

我们之前 _version++  从字典中删除了 when Remove'ing。当只是覆盖字典中现有键的值时,我们也应该考虑这样做。这将启用更新循环来调整字典中的值,而无需求助于复杂且更昂贵的措施。

关于该问题的评论询问:

这样做有什么好处?

Stephen Toub回答说

正如原帖中所说,今天抛出的精细模式将开始正常工作,例如

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

如果您查看Dictionary<, > 源代码,您可以看到该_version字段(用于检测修改)现在仅在特定条件下更新,而不是在修改现有键时更新。

特别感兴趣的领域是TryInsert方法(由索引器调用,见下文)及其第三个 type 参数InsertionBehavior。当此值为现有密钥时InsertionBehavior.OverwriteExisting,版本控制字段不会更新。

例如,看到这一段代码更新 TryInsert

if (behavior == InsertionBehavior.OverwriteExisting)
{ 
    entries[i].value = value;
    return true;
}
Run Code Online (Sandbox Code Playgroud)

更改之前,该部分看起来像这样(我的代码注释):

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

请注意,该_version字段的增量已被删除,因此允许在枚举期间进行修改。

为了完整起见,索引器设置器看起来像这样。此更改并未对其进行修改,但请注意影响上述行为的第三个参数:

set 
{
    bool modified = TryInsert(key, value, InsertionBehavior.OverwriteExisting);
    Debug.Assert(modified);
} 
Run Code Online (Sandbox Code Playgroud)

Remove'ing from the dictionary 也不再影响枚举。然而,自 netcore 3.0 以来一直存在,并在以下文档中Remove适当地指出:

仅适用于 .NET Core 3.0+:可以安全地调用此变异方法,而不会使Dictionary<TKey,TValue> 实例上的活动枚举器失效 。这并不意味着线程安全。

尽管一个开发者在链接的问题,该文件被更新(和这似乎是一个保证,它坚持定),为索引的文档还没有(2021年4月4日)进行了更新,以反映当前的行为。