在C#语言里,yield关键字常常被初学者误认为只是简化返回列表的写法,其实它背后是一整套迭代器状态机机制。理解yield的工作方式,不仅能帮你写出更省内存的数据处理逻辑,还能用纯C#模拟出类似游戏开发里协程的暂停与恢复能力。
yield到底是什么
当在一个方法中使用yield return或yield break时,编译器并不会让这个方法像普通函数那样顺序执行完再返回数组,而是自动生成一个隐藏的状态机类。这个类实现了IEnumerable和IEnumerator接口,方法的每一段代码被切分成不同的状态编号。
调用包含yield的方法本身不会立刻执行方法体,它只是返回一个迭代器对象。只有当你用foreach循环或者手动调用MoveNext时,代码才会跑到下一个yield return处暂停,并把当前值交出来。下次再调用MoveNext,就从暂停点继续执行。这种惰性求值特性,是yield最核心的价值。
using System;
using System.Collections.Generic;
class Demo
{
static IEnumerable<int> CountToThree()
{
Console.WriteLine("开始");
yield return 1;
Console.WriteLine("到1");
yield return 2;
Console.WriteLine("到2");
yield return 3;
Console.WriteLine("结束");
}
static void Main()
{
var it = CountToThree();
Console.WriteLine("还没进方法体");
foreach (var n in it)
{
Console.WriteLine(n);
}
}
}
上面这段代码运行后会先打印“还没进方法体”,然后才逐步进入CountToThree。这说明方法体是按需执行的,而不是一次性算完。
用yield实现自定义迭代器
在没有yield之前,想写一个自定义迭代器需要实现完整的IEnumerator,要写Current、MoveNext、Reset等成员,非常繁琐。yield让编译器代劳了这些样板代码,你只需关心每次产出什么数据。
比如我们要从一个大文件里逐行读取并处理,如果一次性读入内存可能造成溢出。用yield可以按行产出,调用方一边遍历一边处理,内存占用始终很小。
using System;
using System.Collections.Generic;
using System.IO;
class FileReader
{
static IEnumerable<string> ReadLinesLazy(string path)
{
using (var sr = new StreamReader(path))
{
string line;
while ((line = sr.ReadLine()) != null)
{
yield return line;
}
}
}
static void Main()
{
foreach (var line in ReadLinesLazy("test.txt"))
{
if (line.Contains("error"))
{
Console.WriteLine(line);
}
}
}
}
这个例子里,即使文件有几十GB,只要每行不算太长,程序也能平稳运行。因为任意时刻内存里只有当前这一行。这就是迭代器最实用的场景之一。
yield与协程的关系
严格来说C#语言层面并没有像Unity那样名为协程的关键字,但借助yield的状态暂停能力,可以模拟出协程行为。思路是把一个长时间任务拆成多步,每步用yield return让出执行权,外层调度器在合适时机继续推进。
下面写一个简单的任务调度器,它顺序执行多个返回迭代器的“协程”方法,每次只推进一格,从而表现出协作式多任务的效果。
using System;
using System.Collections.Generic;
class CoroutineDemo
{
static IEnumerable<int> TaskA()
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine("A步" + i);
yield return i;
}
}
static IEnumerable<int> TaskB()
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine("B步" + i);
yield return i;
}
}
static void Run(params IEnumerable<int>[] tasks)
{
var enumerators = new List<IEnumerator<int>>();
foreach (var t in tasks)
{
enumerators.Add(t.GetEnumerator());
}
bool any;
do
{
any = false;
foreach (var e in enumerators)
{
if (e.MoveNext())
{
any = true;
}
}
} while (any);
}
static void Main()
{
Run(TaskA(), TaskB());
}
}
运行后你会看到A和B的步骤交替出现,这便是用yield实现的极简协程调度。在真实项目里,可以把yield return后面换成等待条件、时间段或子协程,做出更复杂的流程控制。
常见误区与注意点
第一个误区是以为yield方法返回的集合可以反复遍历。实际上迭代器是一次性消费,第二次foreach会重新走一遍方法体,如果方法里有读文件或取随机数,结果可能不同甚至报错。需要多次使用时,用ToList缓存起来。
第二个误区是在yield方法里写return语句。yield方法里只能用yield return或yield break,普通return会导致编译错误。另外,yield不能出现在匿名方法或unsafe块中,这些限制都是状态机生成规则决定的。
| 写法 | 是否合法 | 说明 |
|---|---|---|
| yield return 1; | 合法 | 产出单个元素并暂停 |
| yield break; | 合法 | 提前结束迭代 |
| return list; | 不合法 | yield方法中禁止普通return |
| 在lambda里写yield | 不合法 | 状态机无法嵌入匿名函数 |
掌握这些边界,你才能在团队代码里放心使用yield,既写出简洁的迭代器,又不会埋下隐蔽的逻辑坑。
总结
yield关键字远不止是少写几行代码的语法糖。它基于编译器生成的状态机,把方法切成可暂停的片段,从而支撑惰性迭代与协程式调度。在数据处理、流式读取、任务编排中都有极高实用价值。只要避开一次性遍历和错误返回写法,就能把这块特性用得很稳。