Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dictionary methods Remove and Clear (.NET Core) modify the collection during enumeration. No exception thrown

I am trying to implement a caching mechanism for enumerating collections safely, and I am checking if all modifications of the built-in collections are triggering an InvalidOperationException to be thrown by their respective enumerators. I noticed that in the .NET Core platform the Dictionary.Remove and Dictionary.Clear methods are not triggering this exception. Is this a bug or a feature?

Example with Remove:

var dictionary = new Dictionary<int, string>();
dictionary.Add(1, "Hello");
dictionary.Add(2, "World");
foreach (var entry in dictionary)
{
    var removed = dictionary.Remove(entry.Key);
    Console.WriteLine($"{entry} removed: {removed}");
}
Console.WriteLine($"Count: {dictionary.Count}");

Output:

[1, Hello] removed: True
[2, World] removed: True
Count: 0

Example with Clear:

var dictionary = new Dictionary<int, string>();
dictionary.Add(1, "Hello");
dictionary.Add(2, "World");
foreach (var entry in dictionary)
{
    Console.WriteLine(entry);
    dictionary.Clear();
}
Console.WriteLine($"Count: {dictionary.Count}");

Output:

[1, Hello]
Count: 0

The expected exception is:

InvalidOperationException: Collection was modified; enumeration operation may not execute.

...as is thrown by the method Add, and by the same methods in .NET Framework.

.NET Core 3.0.0, C# 8, VS 2019 16.3.1, Windows 10

like image 363
Theodor Zoulias Avatar asked Oct 30 '19 00:10

Theodor Zoulias


2 Answers

This appears to be an intentional difference between .Net full framework and .Net core for Dictionary<TKey, TValue>.

The divergence occurred in Pull #18854: Remove version increment from Dictionary.Remove overloads:

Removes the version increment from Remove operations

This addresses the coreclr side of the api change Add Dictionary.Remove(predicate) with the intention of allowing removal of items from the dictionary while enumerating per direction from @jkotas . All collections tests and modified and new tests added in the related corefx PR.

There appears to be an open documentation issue:

Issue #42123: Clarify Dictionary behavior/guarantees around mutation during enumeration:

Is it correct to say that the current implementation of Dictionary supports non-concurrent mutation during iteration?

Only removal. This was enabled as a feature in dotnet/coreclr#18854.

is this something that can be depended on going forward

Yes.

We should ensure the docs are updated to reflect this.

You might want to add a vote to the open doc issue requesting clarification as the .Net core 3.0 documentation for Dictionary<TKey,TValue>.GetEnumerator() is now obsolete:

If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and the next call to MoveNext or IEnumerator.Reset throws an InvalidOperationException.

Strangely enough, the enumerator for SortedDictionary<TKey, TValue> does throw when the dictionary is modified during enumeration.

Demos:

  • .Net framework Remove(): https://dotnetfiddle.net/8vONOw (throws).
  • .Net core Remove(): https://dotnetfiddle.net/es6STm (does not throw).
  • .Net core Add(): https://dotnetfiddle.net/6q7Lvx (throws).
  • .Net core Remove() from SortedDictionary<int, string>: https://dotnetfiddle.net/bssrG7 (throws).
like image 65
dbc Avatar answered Sep 22 '22 13:09

dbc


To clarify when the change was made available and via which methods...

Microsoft's docs on the Dictionary's .Remove and .Clear have been updated:

.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.

.NET Core 3.0 came with C# 8.0. Since then, we have been able to modify a Dictionary<TKey,TValue> during enumeration (foreach) via .Remove and .Clear only.

like image 29
Super Jade Avatar answered Sep 18 '22 13:09

Super Jade