Enumerable.Select返回空枚举?为何其枚举器表现异于其他枚举器?
问题描述
我正在编写一个库函数,用于将枚举拆分为多个枚举段,但遇到了Enumerable.Select语句返回的枚举器存在无法解释的问题。请查看以下代表性代码示例及输出。这似乎是结构体枚举器的问题,但我始终无法定位问题以找到解决方法,恳请帮助!
代码示例
class Program { private static IEnumerable<IEnumerable<T>> wrap<T>(IEnumerable<T> collection) { return wrapped(); IEnumerable<IEnumerable<T>> wrapped() { Console.Write($"{collection.Count()} items: "); using (var etor = collection.GetEnumerator()) { if (!(etor is object)) throw new Exception(); yield return inner(); IEnumerable<T> inner() { while (etor.MoveNext()) yield return etor.Current; } } } } static void Main(string[] args) { var c1 = Enumerable.Range(0, 5); var c2 = Enumerable.Range(0, 5).Select(x => x); var c3 = Enumerable.Range(0, 5).Select(x => x).ToList(); print(wrap(c1).First()); // ok print(wrap(c2).First()); // empty! print(wrap(c3).First()); // ok Console.WriteLine("<done>"); Console.ReadKey(); void print<T>(IEnumerable<T> tt) => Console.Write(string.Join(", ", tt) + "\n"); } }
控制台输出
5 items: 0, 1, 2, 3, 4 5 items: 5 items: 0, 1, 2, 3, 4 <done>
问题原因
问题出在结构体类型的枚举器(比如Enumerable.Select返回的枚举器)在迭代器方法中被闭包捕获时的行为。当你用var etor = collection.GetEnumerator()声明枚举器时,对于Select返回的序列,etor是一个值类型(结构体)。在使用yield return的迭代器方法中,局部变量会被存储到状态机对象中,值类型的枚举器在捕获和状态保存过程中会被复制,导致内部函数inner访问的是一个无效的枚举器副本,调用MoveNext()直接返回false。
解决方案
最简单的修复方式是将枚举器显式声明为接口类型IEnumerator<T>,这样结构体枚举器会被装箱为引用类型,确保内部函数访问的是同一个枚举器实例:
private static IEnumerable<IEnumerable<T>> wrap<T>(IEnumerable<T> collection) { return wrapped(); IEnumerable<IEnumerable<T>> wrapped() { Console.Write($"{collection.Count()} items: "); // 显式声明为接口类型,将结构体枚举器装箱为引用类型 using (IEnumerator<T> etor = collection.GetEnumerator()) { if (!(etor is object)) throw new Exception(); yield return inner(); IEnumerable<T> inner() { while (etor.MoveNext()) yield return etor.Current; } } } }
修改后,c2的输出会正常显示0, 1, 2, 3, 4。
原理说明
- 当枚举器被声明为
IEnumerator<T>接口类型时,值类型的结构体枚举器会被装箱到堆上,成为引用类型。 - 闭包捕获引用类型变量时,会直接指向堆上的同一个实例,避免了值类型复制导致的状态不一致问题。
- 这种修改的装箱开销可以忽略,不会对性能造成明显影响。
内容的提问来源于stack exchange,提问作者JohnC
相关产品推荐
相关产品推荐

