You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 03:15:57