There was a change to the source code of Dictionary<TKey, TValue>
to allow updates of existing keys during enumeration. It was commited on April 9, 2020 by Stephen Toub. That commit can be found here along with corresponding PR #34667.
The PR is titled "Allow Dictionary overwrites during enumeration" and notes that it fixes issue #34606 "Consider removing _version++
from overwrites in Dictionary<TKey, TValue>
". The text of that issue, opened by Mr. Toub is as follows:
We previously removed the? _version++
?when Remove'ing from a
dictionary. We should consider doing so as well when just overwriting
a value for an existing key in the dictionary. This would enable
update loops that tweak a value in the dictionary without needing to
resort to convoluted and more expensive measures.
A comment on that issue asks:
What is the benefit of doing this?
To which Stephen Toub replied:
As called out in the original post, fine patterns that are currently throwing today will start working correctly, e.g.
foreach (KeyValuePair<string, int> pair in dict)
dict[pair.Key] = pair.Value + 1;
If you look at the Dictionary<, >
source code, you can see that the _version
field (which is used to detect modifications) is now only updated under certain conditions and not when an existing key is modified.
The area of particular interest is the TryInsert
method (which is called by the indexer, see below) and its third parameter of type InsertionBehavior
. When this value is InsertionBehavior.OverwriteExisting
the versioning field is not updated for an existing key.
For example, see this section of code from the updated TryInsert
:
if (behavior == InsertionBehavior.OverwriteExisting)
{
entries[i].value = value;
return true;
}
Prior to the change that section looked like this (code comment mine):
if (behavior == InsertionBehavior.OverwriteExisting)
{
entries[i].value = value;
_version++; // <-----
return true;
}
Note that the increment of the _version
field has been removed, thus allowing modifications during enumeration.
For completeness, the setter of the indexer looks like this. It was not modified by this change, but note the third parameter which influences the above behavior:
set
{
bool modified = TryInsert(key, value, InsertionBehavior.OverwriteExisting);
Debug.Assert(modified);
}
Remove
'ing from the dictionary no longer impacts enumeration either. That, however, has been around since netcore 3.0 and is appropriately called out in the documentation of Remove
:
.NET Core 3.0+ only: this mutating method may be safely called without
invalidating active enumerators on
the?Dictionary<TKey,TValue>
?instance. This does not imply thread
safety.
Despite one developer's insistence in the linked issue that the documentation be updated (and what appears to be an assurance that it would be), the docs for the indexer have not yet (2021-04-04) been updated to reflect the current behavior.