In .NET<5 and .NET Core 3.1 the following code
var d = new Dictionary<string, int> { { "a", 0 }, { "b", 0 }, { "c", 0 } };
foreach (var k in d.Keys)
{
d[k]+=1;
}
throws
System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
When targeting .NET 5 the snippet no longer throws.
What has changed?
I failed to find the answer in Breaking changes in .NET 5 and Performance Improvements in .NET 5.
Is it something to do with ref readonly T
?
I think the dictionary only has one key value.
ContainsValue() Method in C# The Dictionary. ContainsValue() method in C# is used to check whether the Dictionary<TKey,TValue> contains a specific value or not.
A dictionary, also called an associative array, is a collection of unique keys and a collection of values, where each key is associated with one value. Retrieving and adding values is very fast. Dictionaries take more memory because for each value there is also a key.
In Dictionary, key must be unique. Duplicate keys are not allowed if you try to use duplicate key then compiler will throw an exception. In Dictionary, you can only store same types of elements. The capacity of a Dictionary is the number of elements that Dictionary can hold.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With