yield return 实现的是迭代器协议而非协程,本质是编译器生成的同步状态机,仅支持惰性求值,不涉及线程挂起或异步调度。

yield return 不是协程,但行为像轻量级协程
别被“协程”这个词带偏——C# 里没有原生协程(不像 Unity 的 IEnumerator + StartCoroutine 那套),yield return 实现的是**迭代器协议**,底层是编译器生成的状态机。它不抢占线程、不挂起栈、也不依赖调度器,只是在每次 MoveNext() 调用时恢复上次暂停的位置。你写 yield return,实际是在定义一个可暂停/恢复的序列生成逻辑,不是启动一个并发任务。
常见误解:以为 yield return 会自动异步执行或切换上下文。它完全同步,调用方 foreach 或 ToList() 时才真正执行并阻塞当前线程。
- 想做真异步流?必须用
IAsyncEnumerable<t></t>+yield return+async方法(.NET Core 3.0+) - Unity 中的
IEnumerator协程能“等待帧”或“等待秒”,那是因为MonoBehaviour.StartCoroutine主动解释了yield return new WaitForSeconds(1)这类特殊对象,和语言级yield无关 - 普通控制台或 ASP.NET 环境下,
yield return就是纯同步惰性求值,别指望它帮你释放线程
yield return 方法签名和返回类型不能乱写
yield return 方法不是想返回啥就返回啥。编译器强制要求:返回类型必须是 IEnumerable<t></t>、IEnumerable、IEnumerator<t></t> 或 IEnumerator;不能是 List<t></t>、T[] 或 void(除非是 void 的 get 访问器,但极少用)。
错误示例:public List<string> GetNames() { yield return "a"; }</string> → 编译失败 CS1622:“无法使用 yield return 语句返回值,因为该方法的返回类型不是 ‘iterator interface’”
- 泛型优先:用
IEnumerable<string></string>而不是IEnumerable,避免装箱和运行时类型检查开销 - 别返回
IEnumerator<t></t>给调用方直接用:它不支持多次遍历,且容易漏掉Dispose();让调用方用foreach或 LINQ,由框架自动管理生命周期 - 如果需要参数化行为(比如分页大小、过滤条件),把它们作为方法参数传入,而不是试图在迭代器里读取外部变量快照
循环中捕获局部变量是高频翻车点
这个坑太经典:foreach 循环内新建对象却反复赋给同一个变量,再 yield return 它——结果所有返回项都指向最后一个实例。
错误代码:
public static IEnumerable<FileInfo> BadEnumerate(string[] paths)
{
var item = new FileInfo("");
foreach (var p in paths)
{
item = new FileInfo(p); // ❌ 全部 yield 返回的都是 item 的最后一次赋值
yield return item;
}
}正确写法是每次 yield 前构造新值,或直接计算字段:
-
yield return new FileInfo(p);(推荐,无状态、无引用共享) -
yield return new FileInfo(p).FullName;(如果只需要字符串) - 若必须复用对象逻辑,把构建过程封装成独立方法,确保每次调用都返回新实例
根本原因:编译器把局部变量提升为状态机类的字段,所有 yield return 共享同一内存地址。这不是闭包问题,是状态机设计使然。
真实场景中别硬套 yield,先想清楚是否真需要惰性
yield return 的价值只在「数据源大、生成代价高、消费者未必全用」时才凸显。如果集合很小(ToList() 或 ToArray(),那直接 return new List<t> { ... }</t> 更清晰、GC 更友好、调试也更容易。
典型值得用的场景:
- 逐行读大文件:
ReadLinesChunked(path)避免整文件进内存 - 递归遍历深层目录树:
EnumerateFilesRecursive(root)不用显式栈,天然深度优先+惰性 - 按需生成无限序列(如斐波那契):
Fibonacci()可以Take(100)而不爆栈
容易被忽略的点:一旦用了 yield,方法体内的 try/catch 和 using 就得格外小心。比如在 try 块里 yield return,编译器会报错 CS1626;资源释放必须靠 finally 或 using 在 yield 外围包裹,否则 Dispose 可能不触发。


















