为什么这个 foreach 循环不抛出 "collection was modified" 异常?

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

我正在使用 .NET Framework 4.0。

神奇之处在于 OrderBy() LINQ 方法。这里有几个例子供您参考:

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

如预期的那样,此循环抛出 "System.InvalidOperationException: Collection was modified; enumeration operation may not execute." 异常。

但是,如果我添加对 OrderBy() 的调用:

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

代码执行得很好,从列表中删除了所有偶数。

起初我假设 OrderBy() 只是枚举了源列表并创建了它的排序副本。这是有道理的,并解释了为什么循环不抛出异常:我没有枚举我正在修改的相同列表。但是在 the documentation(“备注”部分)中指出:

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.

这是文档中的错误(可能是不小心复制粘贴了这个块?)还是我遗漏了什么?

P.S。 this question, but the most upvoted answer 假定 OrderBy() 只是枚举了列表。我很想知道它是否真实(对某些 .NET 源的引用 非常欢迎)。也许副本确实不是创建的,但在我修改之前源列表已完全枚举?

我认为这是因为当 OrderBy 开始执行时,它通过调用 ToArrayBuffer<T> class 中创建了列表的副本,因此修改了原始列表列表不会抛出异常。这是一个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);
        }
    }

BufferGetEnumerator方法中初始化:

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]];
            }
        }
    }

文档中没有错误 - list.OrderBy(v => v) 被推迟(注意在此处执行),但是当您使用 foreach 迭代它时 OrderBy 将需要处理整个集合以确定所有元素的顺序。

你可以通过在第一个元素处引入副作用和跳出循环来看到它:

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;
} 

internally(.NET Framework版本)OrderBy在执行过程中会创建一个传入的可枚举的副本:

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]];
    }
}