【游戏开发】C# 中的迭代器


C# 中的迭代器

一、核心结论

C# 中的迭代器(Iterator) ,本质上是一种按需、逐个产生数据的遍历机制。

最常见的实现方式就是:

csharp 复制代码
yield return

例如:

csharp 复制代码
IEnumerable<int> GetNumbers()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

然后:

csharp 复制代码
foreach (var n in GetNumbers())
{
    Debug.Log(n);
}

这里 GetNumbers() 并不会一次性把 1、2、3 全部计算出来,而是 foreach 每取一个元素,就继续执行到下一个 yield return。

所以面试里最重要的一句话是:

迭代器的核心是延迟执行 + 状态保存,通过 yield return 可以把一个方法变成可逐步遍历的数据源。


二、迭代器是怎么工作的

看这个例子:

csharp 复制代码
IEnumerable<int> GetNumbers()
{
    Debug.Log("Start");

    yield return 1;

    Debug.Log("After 1");

    yield return 2;

    Debug.Log("After 2");

    yield return 3;
}

调用:

csharp 复制代码
var numbers = GetNumbers();
Debug.Log("A");

这时候实际上:

text 复制代码
GetNumbers()
本体不会真正执行

真正开始执行是在:

csharp 复制代码
foreach (var n in numbers)

或者:

csharp 复制代码
var enumerator = numbers.GetEnumerator();
enumerator.MoveNext();

之后。

执行过程可以理解成:

text 复制代码
MoveNext()
   ↓
执行到 yield return 1
   ↓
暂停
   ↓
当前状态保存下来

再次 MoveNext()
   ↓
从上次暂停的位置继续
   ↓
yield return 2

所以迭代器本质上包含两件事情:

text 复制代码
数据产生
+
遍历状态保存

三、yield return 为什么特别重要

yield return 最大的作用就是:

暂停当前方法,把一个值返回给调用者,下次继续从暂停的位置执行。

例如:

csharp 复制代码
IEnumerable<int> Test()
{
    Debug.Log("1");

    yield return 10;

    Debug.Log("2");

    yield return 20;

    Debug.Log("3");
}

执行:

csharp 复制代码
var e = Test().GetEnumerator();

e.MoveNext();  // 输出 1,返回 10
e.MoveNext();  // 输出 2,返回 20
e.MoveNext();  // 输出 3,结束

它和普通 return 最大的区别是:

text 复制代码
return
→ 方法结束

yield return
→ 暂停方法
→ 返回当前值
→ 下次继续执行

四、迭代器的底层原理

这个是 Unity 面试中比较容易被追问的地方。

编译器并不会真的让原方法"暂停在某一行"。

实际上,C# 编译器会把包含 yield 的方法转换成一个状态机(State Machine)。

概念上类似:

text 复制代码
Iterator State Machine

state = 0
   ↓
MoveNext()
   ↓
yield return 1
   ↓
state = 1
   ↓
return true

再次 MoveNext()
   ↓
根据 state = 1
   ↓
继续执行
   ↓
yield return 2

所以面试官如果问:

"yield return 是怎么实现的?"

可以回答:

编译器会把迭代器方法转换成一个状态机,通过 IEnumerator 的 MoveNext() 推进执行,并保存当前迭代状态。


五、IEnumerable 和 IEnumerator 的区别

这个非常高频。

IEnumerable

代表:

"我可以被遍历。"

例如:

csharp 复制代码
IEnumerable<int>

它主要提供:

csharp 复制代码
GetEnumerator()

拿到一个迭代器。

IEnumerator

代表:

"我当前正在怎么遍历。"

核心是:

csharp 复制代码
MoveNext()
Current

关系可以记成:

text 复制代码
IEnumerable
    ↓ GetEnumerator()
IEnumerator
    ↓
MoveNext()
    ↓
Current

所以:

IEnumerable 是数据源,IEnumerator 是遍历过程。


六、使用迭代器需要注意什么?

1. 注意延迟执行

这是最重要的问题之一。

例如:

csharp 复制代码
IEnumerable<int> GetNumbers()
{
    Debug.Log("Execute");
    yield return 1;
}

执行:

csharp 复制代码
var result = GetNumbers();

并不会打印:

text 复制代码
Execute

真正遍历时才执行。

所以:

csharp 复制代码
var result = GetNumbers();

foreach (var item in result)
{
}

才会真正触发逻辑。

面试要点

使用 IEnumerable 时要注意它可能是延迟执行的,方法调用本身不代表数据已经计算完成。


七、重复遍历可能导致代码重复执行

例如:

csharp 复制代码
IEnumerable<int> GetNumbers()
{
    Debug.Log("Generate");
    yield return 1;
    yield return 2;
}

然后:

csharp 复制代码
var result = GetNumbers();

foreach (var x in result)
{
}

foreach (var x in result)
{
}

你可能以为数据已经保存了。

实际上:

text 复制代码
第一次 foreach
→ 执行一次迭代器

第二次 foreach
→ 可能重新创建 Enumerator
→ 重新执行迭代器

因此:

IEnumerable 不一定代表一个已经计算好的集合。

如果需要缓存结果,可以:

csharp 复制代码
var list = GetNumbers().ToList();

之后遍历 list。


八、注意迭代器的性能和 GC

在 Unity 中尤其重要。

例如:

csharp 复制代码
foreach (var item in someEnumerable)
{
}

虽然 foreach 很方便,但具体是否产生 GC、是否装箱,取决于遍历对象的具体类型和实现。

尤其要注意:

text 复制代码
值类型 Enumerator
+
接口 IEnumerable / IEnumerator
+
装箱

可能产生额外开销。

所以在 Unity 高频代码,比如:

text 复制代码
Update
LateUpdate
战斗循环
大量对象遍历

不要简单认为:

foreach 一定零 GC。

应该结合具体集合类型和 Profiler 验证。


九、不要在热点逻辑中频繁创建复杂迭代器

例如:

csharp 复制代码
void Update()
{
    var result = GetEnemies()
        .Where(x => x.IsAlive)
        .Select(x => x.Target);
}

这里不仅有迭代器,还有 LINQ、Lambda、延迟执行等机制。

如果这是每帧执行的代码,就需要警惕:

text 复制代码
迭代器开销
+
Lambda
+
LINQ
+
GC

Unity 项目中常见做法是:

非热点逻辑可以追求可读性;高频性能代码则优先控制分配和遍历成本。


十、不要在迭代过程中随意修改集合

例如:

csharp 复制代码
foreach (var item in list)
{
    list.Remove(item);
}

很多集合会直接抛出:

text 复制代码
InvalidOperationException

因为 Enumerator 通常会检测集合版本变化。

更安全的方式可以是:

csharp 复制代码
for (int i = list.Count - 1; i >= 0; i--)
{
    if (NeedRemove(list[i]))
    {
        list.RemoveAt(i);
    }
}

或者:

csharp 复制代码
var removeList = ...

遍历结束后统一修改。

这个属于 foreach / Enumerator 的实际使用注意点,面试时提一下比较加分。


十一、Unity 中还有一个特殊点:不要把 C# 迭代器和 Unity 协程完全混为一谈

Unity 中经常看到:

csharp 复制代码
IEnumerator Start()
{
    yield return new WaitForSeconds(1);
}

很多人会说:

"IEnumerator 就是协程。"

这个说法不准确。

更准确的是:

C# 的 IEnumerator 是迭代机制;Unity 在此基础上使用 IEnumerator + yield 实现协程调度。

例如:

csharp 复制代码
IEnumerator Test()
{
    Debug.Log("A");

    yield return new WaitForSeconds(1);

    Debug.Log("B");
}

这里 yield return 本质上仍然是在产生一个迭代结果,而 Unity 的协程系统会拿到这个结果,根据 WaitForSeconds 等对象决定什么时候继续调用 MoveNext()。

可以理解成:

text 复制代码
C# Iterator
    ↓
IEnumerator
    ↓
Unity 协程调度器
    ↓
MoveNext()
    ↓
继续执行

这是 Unity 面试很常见的加分点。


十二、面试高频追问

Q1:yield return 和 return 有什么区别?

return 直接结束方法;yield return 暂停迭代器,把当前值返回,下次通过 MoveNext() 从当前位置继续执行。

Q2:IEnumerable 和 IEnumerator 有什么区别?

IEnumerable 表示一个可遍历的数据源,通过 GetEnumerator() 获取 IEnumerator;IEnumerator 负责具体的遍历状态,通过 MoveNext() 和 Current 获取数据。

Q3:yield return 为什么可以暂停方法?

编译器会把迭代器方法转换为状态机,通过保存状态记录当前执行位置,每次 MoveNext() 时从对应状态继续执行。

Q4:IEnumerable 是不是数据已经算好了?

不一定。它可能采用延迟执行,真正遍历时才执行计算,所以重复遍历可能重复执行生成逻辑。

Q5:Unity 协程和 C# 迭代器有什么关系?

Unity 协程基于 IEnumerator 和 yield 机制实现,但 IEnumerator 本身只是 C# 的迭代机制,并不等同于 Unity 协程。


十三、面试口述总结

C# 的迭代器本质上是一种按需产生数据、逐步遍历的机制,最典型的实现就是 yield return。
使用 yield 的方法会被编译器转换成一个状态机,通过 IEnumerator 的 MoveNext() 保存和推进当前执行状态。IEnumerable 表示可遍历的数据源,IEnumerator 负责具体的遍历过程。
使用时需要注意延迟执行、重复遍历可能导致逻辑重复执行,以及 Unity 高频代码中的 GC 和性能问题。
另外 Unity 协程本质上也是建立在 IEnumerator 和 yield 机制之上的,但 IEnumerator 本身并不等于协程。


  • 🎬 博客主页:https://xiaoy.blog.csdn.net

  • 🎥 本文由 呆呆敲代码的小Y 原创 🙉

  • 🎄 学习专栏推荐:Unity系统学习专栏

  • 🌲 游戏制作专栏推荐:游戏制作

  • 🌲Unity实战100例专栏推荐:Unity 实战100例 教程

  • 🏅 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!

  • 📆 未来很长,值得我们全力奔赴更美好的生活✨

  • ------------------❤️分割线❤️-------------------------

资料白嫖,技术互助

学习路线指引(点击解锁) 知识定位 人群定位
🧡 Unity系统学习专栏 入门级 本专栏从Unity入门开始学习,快速达到Unity的入门水平
💛 Unity实战类项目 进阶级 计划制作Unity的 100个实战案例!助你进入Unity世界,争取做最全的Unity原创博客大全。
❤️ 游戏制作专栏 难度偏高 分享学习一些Unity成品的游戏Demo和其他语言的小游戏!
💚 游戏爱好者万人社区 互助/吹水 数万人游戏爱好者社区,聊天互助,白嫖奖品
💙 Unity100个实用技能 Unity查漏补缺 针对一些Unity中经常用到的一些小知识和技能进行学习介绍,核心目的就是让我们能够快速学习Unity的知识以达到查漏补缺
相关推荐
Raas1005 小时前
AI网关能做语义缓存吗?MAI Gateway(魔芋企业级AI网关)实战能力深度解读
java·后端·spring
weixin199701080165 小时前
《1688消息服务落地方案:alibaba.message.* 与 Webhook 回调的可靠性与重试策略》(附Python源码)
开发语言·python
天空鸟_时光不老5 小时前
13-亮点提炼:把技术说辞翻译成业务价值
java·spring boot·spring·spring cloud·mybatis
小蒜学长5 小时前
基于SpringBoot+Vue的租房管理系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·租房管理
ZHOUPUYU5 小时前
PHP 9.0 前瞻:下一代 PHP 会带来哪些新功能?
android·开发语言·php
某不知名網友5 小时前
ROS2 核心通信:话题发布与订阅详解
开发语言·c++
C++ 老炮儿的技术栈5 小时前
char (*csConnectName)[256]; 和 char csConnectName [256] 有什么区别
java·c语言·开发语言·c++·人工智能·算法·c
hez20105 小时前
一行代码在 .NET 项目里启用 Satori GC
c#·.net·.net core·gc·clr
洋就在江州5 小时前
gitlab-cicd 离线集成——springboot-cicd (非docker形式,shell形式)
java·spring boot·后端·ci/cd·gitlab·gitlab-runner
CEZ5 小时前
JeeWMS 开源 WMS 全景导览:一套 Java 仓库管理系统从单据流到代码入口的阅读地图
java·开源