Michał
Michał

Reputation: 2282

Exception when modifying collection in foreach loop

I know the basic principle of not modifying collection inside a foreach, that's why I did something like this:

    public void UpdateCoverages(Dictionary<PlayerID, double> coverages)
    {
        // TODO: temp
        var keys = coverages.Select(pair => pair.Key);
        foreach (var key in keys)
        {
            coverages[key] = 0.84;
        }
    }

And:

class PlayerID : IEquatable<PlayerID>
{
    public PlayerID(byte value)
    {
        Value = value;
    }

    public byte Value { get; private set; }

    public bool Equals(PlayerID other)
    {
        return Value == other.Value;
    }
}

First I save all my keys not to have the Collection modified exception and then I go through it. But I still get the exception which I cannot understand.

How to correct this and what is causing the problem?

Upvotes: 2

Views: 80

Answers (1)

Marc Gravell
Marc Gravell

Reputation: 1062830

First I save all my keys

No you don't; keys is a live sequence that is actively iterating the collection as it is iterated by the foreach. To create an isolated copy of the keys, you need to add .ToList() (or similar) to the end:

var keys = coverages.Select(pair => pair.Key).ToList();

Although personally I'd probably go for:

var keys = new PlayerID[coverages.Count];
coverages.Keys.CopyTo(keys, 0);

(which allows for correct-length allocation, and memory-copy)


What is a live sequence actually?

The Select method creates non-buffered spooling iterator over another... that is a really complicated thing to understand, but basically: when you first start iterating var key in keys, it grabs the inner sequence of coverages (aka coverages.GetEnumerator()), and then every time the foreach asks for the next item, it asks for the next item. Yeah, that sounds complicated. The good news is the C# compiler has it all built in automatically, with it generating state machines etc for you. All mainly done using the yield return syntax. Jon Skeet gives an excellent discussion of this in Chapter 6 of C# in Depth. IIRC this used to be the "free chapter", but now it is not.

However, consider the following:

static IEnumerable<int> OneTwoOneTwoForever()
{
    while(true) {
        yield return 1;
        yield return 2;
    }
}

It might surprise you to learn that you can consume the above, using the same non-buffered "when you ask for another value, it runs just enough code to give you the next value" approach:

var firstTwenty = OneTwoOneTwoForever().Take(20).ToList(); // works!

Upvotes: 3

Related Questions