【问题标题】:Why does this foreach loop NOT throw the "collection was modified" exception?为什么这个 foreach 循环不会抛出“集合已修改”异常?
【发布时间】:2023-03-24 10:59:01
【问题描述】:

我正在使用 .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(“备注”部分)中声明:

这个方法是通过延迟执行来实现的。立即返回值是一个存储执行操作所需的所有信息的对象。在通过直接调用其 GetEnumerator 方法或使用 Visual C# 中的 foreach 或 Visual Basic 中的 For Each 枚举对象之前,不会执行此方法表示的查询。

那么这是文档中的错误(可能是此块的意外复制粘贴?)还是我错过了什么?

附:有this question,但投票最多的answer 假定OrderBy() 只是列举了这个列表。我很想知道它是否属实(对一些.NET源的引用 非常受欢迎)。也许副本确实没有创建,但是在我修改之前完全枚举了源列表?

【问题讨论】:

  • list.OrderBy(v =&gt; v) 返回OrderedEnumerable&lt;TSource&gt;,它会创建自己的枚举器,这就是为什么list.Remove(...) 不会影响list.OrderBy(v =&gt; v)
  • list.ToList() 或 list.ToArray() 也在工作。
  • @Serge ToList() 很明显,因为它创建了集合的副本,因此将使用另一个枚举器。我从未使用过“ToOrder”,所以我不知道它的行为方式。

标签: c# .net linq .net-4.0


【解决方案1】:

我认为这是因为当OrderBy 开始执行时,它会通过调用ToArrayBuffer&lt;T&gt; 类中创建列表的副本,因此修改原始列表不会引发异常。这是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]];
            }
        }
    }

【讨论】:

    【解决方案2】:

    文档中没有错误 - list.OrderBy(v =&gt; 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]];
        }
    }
    

    【讨论】:

    • 感谢“副作用”的想法!
    猜你喜欢
    • 2014-04-07
    • 2014-09-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多