Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What changed in .net 5 that makes it not throw when changing dictionary values in foreach

Tags:

c#

.net-5

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?

like image 501
tymtam Avatar asked Apr 04 '21 09:04

tymtam


People also ask

How many values can a dictionary hold C#?

I think the dictionary only has one key value.

How do you check if a dictionary has any value in C#?

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.

How does dictionary work in C#?

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.

Can C# dictionary have different data types?

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.


1 Answers

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.

like image 122
pinkfloydx33 Avatar answered Oct 24 '22 00:10

pinkfloydx33