19

I'm using a Dictionary<long, bool> and I want to change it while I enumerate through it, but it seems this is not allowed. How can I do this?

Pang
  • 9,564
  • 146
  • 81
  • 122
Dimitar Vouldjeff
  • 2,067
  • 3
  • 19
  • 24
  • Do you want to change the Key or the Value? – H H Feb 27 '10 at 12:23
  • 1
    You would't want to rebuild a bridge while you are on it, yea? You might want to implement in a different way. – o.k.w Feb 27 '10 at 12:24
  • 6
    @[o.k.w] Believe it or not, but that's how huge bridges are built and modified/repaired. And if I have a huge dictionary and must modify each value, I'd rather not clone the keys first. – Roman Starkov Mar 27 '10 at 17:24
  • Seems weird that you can't easily loop over the Keys while updating the Values – lamont Jul 02 '23 at 23:04

8 Answers8

11

Don't, basically. It's explicitly not supported. From the docs for Dictionary<,>.GetEnumerator():

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and its behavior is undefined.

Typically the best way is to remember the modifications you want to make, and then perform them afterwards. Or you could take a copy of the dictionary to start with and then iterate through that while you modify the original. If you could give us more information about what you're trying to do, that would help.

Having said this, the new ConcurrentDictionary class in .NET 4.0 does permit this - but the results of the iteration aren't guaranteed - you may see the changes while you're iterating, or you may not.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    Perhaps more accurately is that they couldn't find a good way to throw IllegalOperationException for ConcurrentDictionary. It is going to cause a *lot* of trouble. I certainly wouldn't recommend it. – Hans Passant Feb 27 '10 at 13:46
  • 1
    @nobugz: I very much doubt that it's because they *couldn't* throw IllegalOperationException. The point of the concurrent collections is that you can do things *concurrently* with them - that includes iterating. Now you usually want to modify the collection in the same thread that's iterating (as that's easy enough to avoid) but in the general case it's much harder. – Jon Skeet Feb 27 '10 at 14:42
  • the why isn't really relevant, the fact that it doesn't throw the exception is. There are a lot of programmers here that take your posts as gospel, I have to urge you to consider the ramifications of your advice. Mutating the dictionary while iterating it, whether by the same thread or another, has Heisenbug written all over it. From looking at the reflected source code, ConcurrentDirectionary does nothing to avoid mishaps. And the MSDN docs are completely silent about it. – Hans Passant Feb 27 '10 at 15:30
  • 2
    The MSDN docs *aren't* completely silent about it. From http://msdn.microsoft.com/en-us/library/dd287131(VS.100).aspx - "The enumerator returned from the dictionary is safe to use concurrently with reads and writes to the dictionary, however it does not represent a moment-in-time snapshot of the dictionary. The contents exposed through the enumerator may contain modifications made to the dictionary after GetEnumerator was called." Not exactly silent, is it? I would expect developers to use ConcurrentDictionary when they specifically need concurrency - and to use it appropriately. – Jon Skeet Feb 27 '10 at 15:54
  • 2
    Also note that I didn't just blindly say "Use ConcurrentDictionary, it solves this problem." I specified that it *permits* concurrent iteration and modification, but also explicitly mentioned that you may or may not see the changes. I can't see how that would mislead anyone. – Jon Skeet Feb 27 '10 at 15:55
8

You should store the key or object you want to delete and break the loop, and then use the Remove() method to delete the object from the dictionary.

Pang
  • 9,564
  • 146
  • 81
  • 122
Prashant Lakhlani
  • 5,758
  • 5
  • 25
  • 39
5

Ideally, it is not advisable to do it. The GetEnumerator() that is called while iterating to the next item will throw an InvalidOperationException. However, from .netcore 3.1 onwards, this will work when you do a dictionary.Remove() or dictionary.Clear() from inside the loop.

Here is the documentation.

Auro
  • 61
  • 1
  • 8
3

As of .net core 3 and higher, this is now possible to Remove items from the dictionary, without invalidating the enumerator (foreach loop):

From the docs here:

.NET Core 3.0+ only: The only mutating methods which do not invalidate enumerators are Remove and Clear.

You should be able to loop through, and remove elements, without causing an error. Creating a list or array as a workaround is no longer needed.

Blue
  • 22,608
  • 7
  • 62
  • 92
1

I answered it here with respect to queue's but the answer is the same. You can't enumerate with foreach and modify the collection at the same time.

Community
  • 1
  • 1
Restore the Data Dumps
  • 38,967
  • 12
  • 96
  • 122
0

Here's my solution, which depends on System.Linq extensions:

 Dictionary<string, bool> _buttons = new Dictionary<string, bool> {
    { "A Button", false },
    { "B Button", false },
    { "X Button", false },
    { "Y Button", false }
 };
 ...
 foreach (var button in _buttons.ToArray()) {
    var name = button.Key;
    var value = CrossPlatformInputManager.GetButton(name);
    if (value != button.Value) {
        _buttons[name] = value;
    }
 }

It explicitly copies to KeyValuePair[], to avoid the mutation errors.

Patrick Beard
  • 592
  • 6
  • 11
  • 1
    A cheaper solution would be to lazily create a `List` that contains the edits to make to the dictionary, and applies them in a separate loop. Depends on how many values actually change and how big the dictionary is. – Patrick Beard Dec 04 '19 at 15:16
0

I'm new to c# and still learning this stuff myself, but I came up with a possible alternative solution that I haven't seen offered elsewhere.

I made a new HashSet containing the keys of the dictionary, and then iterated over that to make changes to the dictionary.

Dictionary<int, string> ToBeChanged = new Dictionary<int, string> {
    { 3, "foo" },
    { 9, "bar" },
    { 2, "baz" },
    { 4, "quux" }
};

HashSet<int> KeySet = new HashSet<int>(ToBeChanged.Keys);

foreach( var Key in KeySet )
{
    if( Key == 2 )
    {
        ToBeChanged.Remove(Key);
    }
    else
    {
        ToBeChanged[Key] = "Hello";
    }
}

The above will change every value to "Hello" and will delete the entry with the key of 2, without the need for making a copy of the Dictionary<> and making the changes there followed by copying back, or the need to keep track of what you want to change during the loop and then doing a second loop after the initial iteration to make those changes.

If you're working with a SortedDictionary<> and you care what order you're processing items then copy the keys in to a SortedSet<> instead.

As I say, I'm new to c# so it's very possible that I've overlooked something obvious and there's a reason why I've not seen this approach suggested elsewhere. If that's the case, please enlighten me.

dfluff
  • 11
  • 2
-1

I ran into a similar situation at work. I needed to modify a dictionary while iteratively making it smaller until I didn't have keys/values left.

I started by creating a list of "to be removed" keys, but it was painfully slow with all the other logic that i had to implement. Alternatively, I ended up iterating through the dictionary backwards. Since you are traversing the dictionary from back to front, the iteration won't throw an error.

After I take out my work related logic, it looks silly, but I promise it works and looks something like:

    Dictionary<int, List<int>> dict = new Dictionary<int, List<int>>();
    while(dict.Any())
    {
        foreach(var key in dict.Keys.Reverse())
        {
            if(//conditional)
            {
                dict.Remove(key);
            }
            else
            {
                //do something with dict[key]
            }
        }
    }
  • 5
    It's working because you made a copy of the keys and iterated the copy, not because you are iterating backwards. – yoyo Mar 09 '19 at 06:57