yield return 本质是编译器生成状态机实现 IEnumerator,非真正协程;需显式调用 MoveNext() 或 foreach 驱动,Unity 中由 StartCoroutine 每帧调度,纯 C# 需手动调度器,async/await 才是现代推荐异步方案。

yield return 本质不是协程,而是状态机编译器糖
C# 的 yield return 不会创建真正意义上的协程(如 Unity 的 Coroutine 或 C++20 的 co_await),它只是让编译器把方法重写为一个实现了 IEnumerator<t></t> 的状态机类。你调用一次 MoveNext(),它就执行到下一个 yield return 并暂停;下次再调用,才继续往后走——这叫“可枚举的延迟计算”,不是抢占式或协作式调度。
常见误解是以为写个 IEnumerator<int> CountUp()</int> 就等于起了个后台协程,其实它完全不自动运行,必须有人显式驱动(比如用 foreach 或手动调 MoveNext())。
Unity 中用 StartCoroutine 驱动 yield 是最常见误用场景
Unity 的 StartCoroutine 是唯一把 yield return “当协程用”的地方,但它底层仍是靠引擎每帧调用 MoveNext() 实现的。这意味着:
-
yield return null→ 等下一帧 -
yield return new WaitForSeconds(1f)→ 等 1 秒(受 Time.timeScale 影响) -
yield return StartCoroutine(Another())→ 嵌套等待另一个协程完成 - 不能在普通 C# 类(非
MonoBehaviour)里直接用StartCoroutine
如果你试图在非 MonoBehaviour 类中写 yield return 并期望它“自己跑”,那它根本不会执行——没有驱动者,状态机就永远停在初始状态。
纯 C#(无 Unity)模拟协程需手动调度
想脱离 Unity 在控制台或 .NET 库中模拟类似行为,核心是:自己实现一个调度器来轮询 IEnumerator 的 MoveNext()。例如:
public static class CoroutineRunner
{
private static readonly List<IEnumerator> _running = new();
<pre class="brush:php;toolbar:false;">public static void Start(IEnumerator routine) => _running.Add(routine);
public static void Update()
{
for (int i = _running.Count - 1; i >= 0; i--)
{
if (!_running[i].MoveNext())
_running.RemoveAt(i);
}
}}
然后你可以这样写逻辑:
IEnumerator FadeIn(float duration)
{
float elapsed = 0;
while (elapsed < duration)
{
// 模拟“每帧更新”
Console.WriteLine($"Fade progress: {elapsed/duration:P1}");
elapsed += 0.1f;
yield return null; // 这里只是个标记,实际由 Update() 驱动
}
}再在主循环中调用 CoroutineRunner.Start(FadeIn(2f)) 和 CoroutineRunner.Update()。注意:yield return null 在这里毫无特殊含义,纯粹是约定用法,换成 yield return 42 也行,只要调度器忽略返回值即可。
async/await 才是现代 C# 推荐的异步协作方式
如果目标是“不阻塞主线程、分段执行、能 await 外部任务”,async/await 比手搓 yield 协程更可靠:
-
await Task.Delay(1000)真正释放线程,不占用 CPU - 支持异常传播、取消令牌(
CancellationToken) - 编译后也是状态机,但由 .NET 运行时统一管理,无需手动调度
- 和
IAsyncEnumerable<t></t>结合还能写出类似yield return的流式异步迭代
只有当你明确需要“逐帧控制”(比如动画插值、UI 渐变)且运行在 Unity 环境下,才该用 yield return + StartCoroutine。其他情况,优先写 async Task 方法。
最容易被忽略的一点:yield return 方法返回的是 IEnumerator 或 IAsyncEnumerable,它本身不启动执行;哪怕你写了 100 行 yield,不调 MoveNext() 或不 foreach,它连第一行都不会进。这点和 async 方法返回 Task 后立即开始执行(除非是 async lambda 被赋值未调用)有本质区别。










