6

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

Super Jade
  • 5,609
  • 7
  • 39
  • 61
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    Confirmed: .Net full framework demo fiddle here: https://dotnetfiddle.net/8vONOw; .Net core fiddle here: https://dotnetfiddle.net/es6STm. Even weirder, .Net core throws for `SortedDictionary()` as shown here: https://dotnetfiddle.net/bssrG7. You might report an issue. – dbc Oct 30 '19 at 03:16

2 Answers2

5

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:

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 2
    bug report in here: https://github.com/dotnet/corefx/issues/42212 – dev-masih Oct 30 '19 at 04:35
  • @dev-masih Checking `_version` appears to have been intentionally removed. See [Allow Dictionary.Remove during enumeration](https://github.com/dotnet/corefx/issues/29979#issuecomment-399806076). – Lance U. Matthews Oct 30 '19 at 04:37
  • @BACON - yeah, I just found the pull request and updated my answer while you commented. – dbc Oct 30 '19 at 04:37
  • 1
    Another oddity: The [`Dictionary.TrimExcess`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess) method, available only on .NET Core, **does** causes the "Collection was modified" exception, without actually modifying the `Dictionary`! – Theodor Zoulias Oct 30 '19 at 05:08
  • 1
    @TheodorZoulias - likely because *in practice* removing an entry from a `Dictionary` doesn't break an ongoing iteration (since the active entries are in something like a linked list), but trimming the excess does because it rehashes everything. BTW my answer isn't an endorsement of the change, I'm just reporting about it. – dbc Oct 30 '19 at 05:10
  • Yeap, this makes sense. So the "Collection was modified" refers to the internal data structure of the class, not to the externally visible data. – Theodor Zoulias Oct 30 '19 at 05:22
1

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.

Super Jade
  • 5,609
  • 7
  • 39
  • 61