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

yield return 是什么,它不是返回值而是挂起点
yield return 不是普通函数的 return,它不会终止方法执行,而是暂停当前迭代器状态,把值“交出去”,等下一次调用 MoveNext() 时从暂停处继续。这意味着方法体实际被编译成一个实现了 IEnumerable<t></t> 和 IEnumerator<t></t> 的状态机类,你写的代码只是语法糖。
常见误解是把它当“逐个 return”,结果在循环里写了 return 混用,导致后续 yield return 永远不执行 —— 这种写法直接报错或逻辑中断。
- 方法返回类型必须是
IEnumerable<t></t>、IEnumerable、IAsyncEnumerable<t></t>(C# 8+)之一 - 方法体内不能有普通
return语句(除了return;用于提前退出迭代) - 不能在
try块中有yield return,除非catch和finally中没有yield return(编译器限制)
怎么写一个基础 yield return 迭代器方法
最典型场景:把一个计算过程或数据流封装成可枚举对象,避免一次性加载全部数据到内存。
public static IEnumerable<int> 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<int> GetEvenNumbersList(int max)
{
var list = new List<int>();
for (int i = 0; i <= max; i += 2)
{
list.Add(i);
}
return list;
}// ✅ yield return 流式生成
public static IEnumerable<int> GetEvenNumbers(int max)
{
for (int i = 0; i <= max; i += 2)
{
yield return i;
}
}关键区别:
-
List<t></t>版本必须分配足够内存容纳所有元素(比如max = 1000000就要存 50 万整数),且全部算完才返回 -
yield return版本按需计算,内存占用恒定(仅保存当前状态),适合大数据流、IO 边界(如逐行读文件)、或消费者可能提前退出的场景(如.FirstOrDefault()) - 但无法重复遍历(除非显式调用
.ToList()),因为每次调用都新建迭代器
容易踩的坑:闭包捕获和延迟执行陷阱
下面这段代码很常见,但结果不符合直觉:
public static IEnumerable<Func<int>> GetDelegates()
{
var actions = new List<Func<int>>();
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 包裹),容易造成资源泄漏或并发问题。










