C# Yield关键字方法 C#如何使用yield return实现迭代器

yield return 是 C# 中用于定义迭代器方法的关键字,它不终止方法而是暂停执行并返回一个值,由编译器生成状态机实现 IEnumerable;必须返回 IEnumerable 等类型,不可混用普通 return,且受 try/catch 和闭包陷阱等限制。

yield return 是什么,它不是返回值而是挂起点

yield return 不是普通函数的 return,它不会终止方法执行,而是暂停当前迭代器状态,把值“交出去”,等下一次调用 MoveNext() 时从暂停处继续。这意味着方法体实际被编译成一个实现了 IEnumerableIEnumerator 的状态机类,你写的代码只是语法糖。

常见误解是把它当“逐个 return”,结果在循环里写了 return 混用,导致后续 yield return 永远不执行 —— 这种写法直接报错或逻辑中断。

  • 方法返回类型必须是 IEnumerableIEnumerableIAsyncEnumerable(C# 8+)之一
  • 方法体内不能有普通 return 语句(除了 return; 用于提前退出迭代)
  • 不能在 try 块中有 yield return,除非 catchfinally 中没有 yield return(编译器限制)

怎么写一个基础 yield return 迭代器方法

最典型场景:把一个计算过程或数据流封装成可枚举对象,避免一次性加载全部数据到内存。

public static IEnumerable GetEvenNumbers(int max)
{
    for (int i = 0; i <= max; i += 2)
    {
        yield return i;
    }
}

调用时:

foreach (int n in GetEvenNumbers(10))
{
    Console.WriteLine(n); // 输出 0, 2, 4, 6, 8, 10
}

注意:GetEvenNumbers(10) 调用本身不执行循环,只返回一个未启动的迭代器;真正执行从 foreach 第一次调用 MoveNext() 开始。

  • 每次 yield return 后,方法暂停,局部变量(如 i)状态被保留
  • 如果想中途退出,用 yield break;,它相当于“迭代结束”,不是异常
  • 不要在 yield return 后写任何代码(除非是 yield break; 或空

    语句),编译器会报错

yield return 和 List.Add 的性能与内存差异

对比两种实现方式:

// ❌ 先构造完整列表再返回
public static List GetEvenNumbersList(int max)
{
    var list = new List();
    for (int i = 0; i <= max; i += 2)
    {
        list.Add(i);
    }
    return list;
}
// ✅ yield return 流式生成
public static IEnumerable GetEvenNumbers(int max)
{
    for (int i = 0; i <= max; i += 2)
    {
        yield return i;
    }
}

关键区别:

  • List 版本必须分配足够内存容纳所有元素(比如 max = 1000000 就要存 50 万整数),且全部算完才返回
  • yield return 版本按需计算,内存占用恒定(仅保存当前状态),适合大数据流、IO 边界(如逐行读文件)、或消费者可能提前退出的场景(如 .FirstOrDefault()
  • 但无法重复遍历(除非显式调用 .ToList()),因为每次调用都新建迭代器

容易踩的坑:闭包捕获和延迟执行陷阱

下面这段代码很常见,但结果不符合直觉:

public static IEnumerable> GetDelegates()
{
    var actions = new List>();
    for (int i = 0; i < 3; i++)
    {
        yield return () => i; // ❌ 所有委托都返回 3
    }
}

原因:所有 yield return 返回的 lambda 共享同一个变量 i,而迭代器直到遍历时才执行,此时循环早已结束,i == 3

修复方式:在循环内创建局部副本:

for (int i = 0; i < 3; i++)
{
    int localI = i; // ✅ 每次迭代独立副本
    yield return () => localI;
}

另一个坑是误以为 yield return 方法“立即执行”——它其实完全惰性。如果你在 yield return 方法里打开文件、数据库连接或 HTTP 请求,这些资源会在第一次 MoveNext() 时才初始化,且若没正确释放(比如没用 using 包裹),容易造成资源泄漏或并发问题。