为什么foreach搭配OrderBy时不抛出“集合已修改”异常?
问题解答:LINQ OrderBy延迟执行与集合修改异常的机制说明
核心结论
官方文档没有错误,你对运行机制的理解存在偏差,二者并不冲突。
1. 对延迟执行的常见误解
LINQ的延迟执行仅表示OrderBy()方法被调用时不会立刻执行排序逻辑,只会返回存储了查询参数的对象。但排序属于非流式运算,必须拿到所有源元素才能完成排序操作,因此一旦枚举启动(即foreach触发GetEnumerator调用时),OrderBy会第一时间完整读取源序列的所有元素到内部缓冲区,完成排序后才会返回第一个迭代元素,后续整个枚举过程都基于缓冲区的副本运行,不再访问原集合。
你第二段代码的实际执行顺序为:
- foreach启动,触发
OrderBy的查询逻辑执行 OrderBy完整遍历原list,把全部6个元素拷贝到内部数组缓冲区,完成排序- 后续foreach遍历的是缓冲区的副本,和原
list完全独立 - 循环内对原
list的删除操作不会影响当前正在遍历的缓冲区,因此不会抛出集合修改异常
2. .NET Framework 4.0源码佐证
OrderBy返回的OrderedEnumerable<TElement, TKey>对象触发枚举时,会首先创建内部Buffer<TElement>实例,该实例的构造函数会全量读取源序列元素到内部数组,核心实现如下:
internal Buffer(IEnumerable<TElement> source) { TElement[] items = null; int count = 0; // 如果源序列实现了ICollection,直接批量拷贝 ICollection<TElement> collection = source as ICollection<TElement>; if (collection != null) { count = collection.Count; if (count > 0) { items = new TElement[count]; collection.CopyTo(items, 0); } } // 否则逐一遍历所有元素,存入动态扩容的数组 else { using (IEnumerator<TElement> en = source.GetEnumerator()) { if (en.MoveNext()) { count = 0; items = new TElement[4]; do { if (count == items.Length) Array.Resize(ref items, count * 2); items[count++] = en.Current; } while (en.MoveNext()); } } } this.items = items; this.count = count; }
可以明确看到,不管源序列是什么类型,Buffer都会在构造阶段完整遍历所有源元素、生成独立副本,后续排序、迭代全程都不会再访问原集合。
3. 两段代码的行为差异说明
第一段代码直接遍历List<T>本身,List<T>内置的迭代器会在每次MoveNext时校验集合的版本号,Remove操作会修改版本号,因此迭代器会立刻抛出InvalidOperationException。
第二段代码遍历的是OrderBy生成的缓冲区副本,其迭代器只会访问内部数组,不会校验原list的版本号,自然不会触发异常。
内容的提问来源于stack exchange,提问作者TheNightdrivingAvenger
相关产品推荐
相关产品推荐

