【游戏开发】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 是怎么实现的?"

可以回答:

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


五、IEnumerableIEnumerator 的区别

这个非常高频。

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 returnreturn 有什么区别?

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

Q2:IEnumerableIEnumerator 有什么区别?

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

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

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

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

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

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

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


十三、面试口述总结

C# 的迭代器本质上是一种按需产生数据、逐步遍历的机制,最典型的实现就是 yield return
使用 yield 的方法会被编译器转换成一个状态机,通过 IEnumeratorMoveNext() 保存和推进当前执行状态。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的知识以达到查漏补缺
相关推荐
TinyMemory14 分钟前
Java 面向对象核心入门(六):this 关键字完全解析
java·笔记·面向对象·java新手·this关键字
benchmark_cc16 分钟前
K 线数据断层会对回测造成什么影响?从一个缺失交易日看懂量化策略为什么会失真
大数据·开发语言·数据分析·量化·股票数据·quantdash·量化数据源
萧瑟余晖16 分钟前
Java深入解析篇九之JavaStream API详解
java
benchmark_cc21 分钟前
数据 API 稳定性为什么会影响量化策略?从数据获取到信号执行的完整分析
开发语言·python·数据分析·量化·股票数据·quantdash·量化数据源
一只QAQ21 分钟前
c++项目
java·c++·算法
小蒜学长28 分钟前
基于SpringBoot + Vue的智能健身房管理系统的设计与实现(代码+数据库+LW)
java·数据库·vue.js·spring boot·后端
DolitD29 分钟前
云流技术深度剖析:单服务器下如何实现3D应用的多实例并发?
java·服务器·前端·3d·云原生·云计算
jun_bai34 分钟前
Ecplise中发布项目到配置的tomcat中
java·服务器
liangbo734 分钟前
JVM规范第 6 章:Java 虚拟机指令集
java·jvm