Home > other >  Why does this foreach loop NOT throw the "collection was modified" exception?
Why does this foreach loop NOT throw the "collection was modified" exception?

Time:11-30

I'm using .NET Framework 4.0.

The magic is somewhere within the OrderBy() LINQ method. Here's a couple of examples for you:

var list = new List<int> { 1, 2, 3, 4, 5, 6};
foreach (var item in list)
{
    if (item % 2 == 0)
      list.Remove(item);
}

This loop, as expected, throws the "System.InvalidOperationException: Collection was modified; enumeration operation may not execute." exception.

However if I add a call to OrderBy():

foreach (var item in list.OrderBy(v => v))
{
    if (item % 2 == 0)
      list.Remove(item);
}

the code executes just fine, removing all the even numbers from the list.

At first I assumed that OrderBy() just enumerated the source list and created a sorted copy of it. This would make sense and explain why the loop does not throw an exception: I'm not enumerating the same list as I'm modifying. However in the documentation ("Remarks" section) it is stated that:

This method is implemented by using deferred execution. The immediate return value is an object that stores all the information that is required to perform the action. The query represented by this method is not executed until the object is enumerated either by calling its GetEnumerator method directly or by using foreach in Visual C# or For Each in Visual Basic.

So is it an error in the documentation (maybe accidental copy-pasting of this block?) or did I miss something?

P.S. There's this question, but the most upvoted answer assumes that OrderBy() just enumerated the list. I'm curious to know whether it's true or not (a reference to some .NET source is very welcome). Maybe the copy is indeed not created, but the source list is completely enumerated before I modify it?

CodePudding user response:

I think this is because when the OrderBy starts executing it creates a copy of the list in the Buffer<T> class by calling ToArray, therefore modifying the original list doesn't throw an exception. Here is a reference to the source code

 internal Buffer(IEnumerable<TElement> source)
    {
        if (source is IIListProvider<TElement> iterator)
        {
            TElement[] array = iterator.ToArray();
            _items = array;
            _count = array.Length;
        }
        else
        {
            _items = EnumerableHelpers.ToArray(source, out _count);
        }
    }

Buffer is initialized in the GetEnumerator method:

public IEnumerator<TElement> GetEnumerator()
    {
        Buffer<TElement> buffer = new Buffer<TElement>(_source);
        if (buffer._count > 0)
        {
            int[] map = SortedMap(buffer);
            for (int i = 0; i < buffer._count; i  )
            {
                yield return buffer._items[map[i]];
            }
        }
    }

CodePudding user response:

There is no error in the documentation - list.OrderBy(v => v) is deferred (noting is executed here), but when you iterate it with foreach OrderBy will need to process whole collection to determine the order of all elements. You can see it by introducing side effect and breaking out of the loop at first element:

var list = new List<int> { 1, 2, 3, 4, 5, 6};
var xxx = list.OrderBy(v => {
    Console.WriteLine(v);
    return v;
});
// next will print "before" and then all collection elements
Console.WriteLine("before");
foreach (var item in xxx)
{
    break;
} 

And internally (.NET Framework version) OrderBy during the execution will create a copy of incoming enumerable:

public IEnumerator<TElement> GetEnumerator() {
    Buffer<TElement> buffer = new Buffer<TElement>(source); // will copy elements here
    if (buffer.count > 0) {
        EnumerableSorter<TElement> sorter = GetEnumerableSorter(null);
        int[] map = sorter.Sort(buffer.items, buffer.count);
        sorter = null;
        for (int i = 0; i < buffer.count; i  ) yield return buffer.items[map[i]];
    }
}
  • Related