协程技术专题
# 协程与多线程
协程在 Unity3d 中是一个很重要的概念,我们知道,在使用 Unity3d 进行游戏开发时,一般不考虑多线程,那么如何处理一些在主任务之外的需求呢,Unity3d 给我们提供了协程这种方式。使用协程不用考虑同步和锁的问题。虽然协程十分方便和灵活,但不当的使用会使程序产生无法预想的后果,请使用前慎重考虑。
为啥在 Unity3d 中一般不考虑多线程呢,因为在 Unity3d 中,只能在主线程中获取物体的对象、组件、方法,如果脱离这些,Unity3d 的很多功能无法实现,这样一来多线程的存在与否意义就不大了。
既然这样,线程与协程有什么区别呢?对于协程而言,同一时间只能执行一个协程,而线程 (是基于多核CPU) 则是并发的,可以同时有多个线程在运行。两者在内存的使用上是相同的,共享堆,不共享栈。其实对于两者最关键,最简单的区别是微观上线程是并行的,而协程是串行的。
# 协程
协程,从字面意义上理解就是协助程序的意思,我们在主任务进行的同时,需要一些分支任务配合工作来达到最终的效果。协程允许您将任务分散到多个帧中,在 Unity3d 中,协程是一种可以暂停执行并将控制权返回给 Unity3d 的方法,但随后在下一帧中断的地方继续执行。
在 Unity3d 中,协程(Coroutine)是一种非常有用的编程工具,适用于需要在多个帧之间执行的任务,而不阻塞主线程的操作。适用于如下场合:
具体应用场景
- 延时操作
当你需要在某个时间间隔后执行操作时,协程非常方便。例如,玩家击败敌人后延迟几秒钟再播放动画或显示信息。
IEnumerator DelayedAction()
{
yield return new WaitForSeconds(3f); // 延迟 3 秒
Debug.Log("3 秒后执行的操作");
}
2
3
4
5
- 逐帧处理
有些操作可能需要在多个帧中分步进行,比如逐帧更新物体的位移、颜色变化等。通过协程可以分步执行而不需要占用每一帧的计算资源。
IEnumerator MoveObject(Vector3 target)
{
while (Vector3.Distance(transform.position, target) > 0.1f)
{
transform.position = Vector3.Lerp(transform.position, target, Time.deltaTime);
yield return null; // 等待下一帧
}
}
2
3
4
5
6
7
8
- 异步加载资源
在加载大文件或异步加载资源时,使用协程可以避免阻塞游戏的主线程,提升性能。例如,在加载大场景时,可以在后台加载资源而不影响主线程的运行。
IEnumerator LoadSceneAsync()
{
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("NewScene");
while (!asyncLoad.isDone)
{
yield return null; // 等待场景加载完成
}
}
2
3
4
5
6
7
8
- 动画控制
协程在做复杂的动画控制时也非常有用,比如多个动画顺序播放、动画结束后执行其他操作等。通过协程,能有效控制每个动画的开始和结束时间。
IEnumerator PlayAnimationSequence()
{
anim.Play("Jump");
yield return new WaitForSeconds(2f); // 等待 2 秒,跳跃动画完成
anim.Play("Land");
}
2
3
4
5
6
- 逐步消耗资源
如果游戏需要逐渐消耗资源或生命值(例如玩家每秒损失生命值),可以使用协程定时进行。
IEnumerator DrainHealth()
{
while (health > 0)
{
health -= 1;
yield return new WaitForSeconds(1f); // 每秒扣 1 点血
}
}
2
3
4
5
6
7
8
- 循环等待
如果有些任务需要在固定时间间隔内循环执行(比如 AI 行为、周期性任务等),协程能够非常方便地实现。
IEnumerator PerformPeriodicTask()
{
while (true)
{
PerformTask();
yield return new WaitForSeconds(5f); // 每 5 秒执行一次
}
}
2
3
4
5
6
7
8
总结: 协程适用于需要跨多帧执行的任务,比如延时操作、逐步处理、定时任务等。使用协程可以避免阻塞主线程,使游戏运行更流畅。需要注意的是,协程不适合频繁执行的计算密集型操作(这种操作应该放到后台线程中),因为它仍然会占用一定的 CPU 资源。
有时候,如果任务非常简单,直接使用 Invoke 或者定时器也能达到类似的效果,协程更适合稍复杂的、跨多帧的任务。
如果要使用方法调用包含随时间推移的过程动画、事件序列则可以使用协程。如果需要处理长时间异步操作,例如HTTP 传输、资产加载、文件 I/O 等,最好使用协程。
试想当在进行主任务的过程中我们需要一个对资源消耗极大的操作时,如果在一帧中实现这样的操作,游戏就会变得十分卡顿,这个时候,我们就可以通过协程,在一定帧内完成该工作的处理,同时不影响主任务的进行。
在一个 MonoBehaviour 提供的主线程里只能有一个处于运行状态的协程。因为协程不是线程,不是并行的。同一时刻一个脚本实例中可以有多个暂停的协程,但只有一个运行着的协程。同一时刻不同的脚本可以可以运行多个协程。每个 MonoBehaviour 脚本实例都有自己的协程队列。不同的脚本实例(即不同的物体或 GameObject 上挂载的脚本)可以并行运行多个协程。Unity 的协程系统是基于 游戏对象(GameObject)和 脚本实例 的,所以不同的脚本实例完全可以同时启动并执行多个协程。这意味着,多个 GameObject 上的脚本可以各自启动协程,互不干扰。
举个栗子
假设有两个 MonoBehaviour 脚本,分别挂载在不同的 GameObject 上:
// Script 1
public class Script1 : MonoBehaviour
{
void Start()
{
StartCoroutine(MyCoroutine1());
}
IEnumerator MyCoroutine1()
{
yield return new WaitForSeconds(2f);
Debug.Log("Script1 协程执行完毕!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Script 2
public class Script2 : MonoBehaviour
{
void Start()
{
StartCoroutine(MyCoroutine2());
}
IEnumerator MyCoroutine2()
{
yield return new WaitForSeconds(1f);
Debug.Log("Script2 协程执行完毕!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在场景中,如果你把 Script1 和 Script2 分别挂载在两个不同的 GameObject 上,它们会分别运行自己的协程,并且协程是互不干扰的。
Script1 会在 2 秒后输出 "Script1 协程执行完毕!"
Script2 会在 1 秒后输出 "Script2 协程执行完毕!"
关键点:
同一个脚本实例:一个脚本实例内的协程是串行执行的,只能有一个协程处于“执行中”状态,但可以有多个协程处于暂停状态。
不同的脚本实例:不同脚本实例(挂载在不同 GameObject 上)可以独立运行多个协程,互不干扰。
总结:
不同的 MonoBehaviour 实例上的协程是并行的,每个实例管理自己的协程队列。只有同一个脚本实例的协程会在执行时互相排队执行,不能并行。
# 协程原理
首先需要了解协程不是线程,协程依旧是在主线程中进行。然后要知道协程是通过迭代器来实现功能的,通过关键字 IEnumerator 来定义一个迭代方法。在迭代器中,最关键的是 yield 的使用,这是实现我们协程功能的主要途径,通过该关键字,可以使得协程的运行与暂停、记录下一次启动的时间与位置等等。
IEnumerator /ɪ'njuːməreɪtə/
- 注意使用的是 IEnumerator,而不是 IEnumerable。
- 两者之间的区别是 IEnumerator 是非泛型的,也是协程认可的参数。IEnumerable 通过泛型实现的迭代器,协程不使用该迭代器。
定义一个协程:
IEnumerator Fade()
{
for (float f = 1f; f >= 0; f -= 0.1f)
{
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
yield return null;//下一帧继续执行for循环
yield return new WaitForSeconds(0.1f);//0.1秒后继续执行for循环
}
}
2
3
4
5
6
7
8
9
10
11
# 协程如何使用
- 创建一个协程
IEnumerator MethondName(Object obj)
{
yield return null;
}
2
3
4
- 开启一个协程
协程 Start 前需要判断其是否开启了,否则协程会不断地叠加。Unity3d 在调用 StartCoroutine() 后不会等待协程中的内容返回,会立即执行后续代码。在 Unity3d 中,使用 MonoBehaviour.StartCoroutine() 方法即可开启一个协程,也就是说该方法必须在 MonoBehaviour 或继承于 MonoBehaviour 的类中调用。
//直接通过方法名启动一个协程,性能消耗会更大一点
Coroutine StartCoroutine(string methondName);
//通过迭代器类型的方法启动协程,这种启动的好处是可以传递多个参数,
//但是不能用 StopCoroutine(string methodName) 来结束,除非使用StopAllCoroutines方法
Coroutine StartCoroutine(IEnumerator routine);
//通过方法名和一个参数启动协程 (只能有一个参数)
Coroutine StartCoroutine(string methondName, object values);
Coroutine StartCoroutine_Auto(IEnumerator routine);
2
3
4
5
6
7
8
9
10
11
- 关闭一个协程
StopCoroutine(string methondName);
StopCoroutine(IEnumerator routine);
StopCoroutine(Coroutine routine);
2
3
- 关闭所有协程
StopAllCoroutines();
Mono 所在的 GameObject 的 enable 属性设置为 false 可以终止该 Mono 中定义的所有协程。当再次设置 enable 为 ture 时,协程并不会再开启。
在一个协程开始后,同样会对应一个结束协程的方法 StopCoroutine 与 StopAllCoroutines 两种方式,但是需要注意的是,两者的使用需要遵循一定的规则,在介绍规则之前,同样介绍一下关于StopCoroutine重载:
//通过方法名(字符串)来进行
StopCoroutine(string methodName);
//通过方法形式来调用
StopCoroutine(IEnumerator routine);
//通过指定的协程来关闭
StopCoroutine(Coroutine routine);
2
3
4
5
6
7
8
如果我们是使用StartCoroutine(string methodName)来开启一个协程,那么结束协程就只能使用 StopCoroutine(string methodName) 和 StopCoroutine(Coroutine routine)来结束协程。而 StopCoroutine(IEnumerator routine) 不能用 StopCoroutine(string methodName) 结束,只能等待执行完毕或者使用 StopAllCoroutines 来终结。还有就是 StopCoroutine(Coroutine routine) 不如 StopCoroutine(string methodName) 方便,所以还是使用 StopCoroutine(string methodName) 比较好。
# yield关键字
//暂停协程等待下一帧继续执行
yield return null;
//0或其他数字,暂停协程等待下一帧继续执行
yield return 0;
//等异步操作结束后再执行后续代码
yield return asyncOperation;
//等待规定时间后继续执行
yield return new WaitForSeconds(时间值);
//等待某个协程执行完毕后再执行后续代码
yield return StartCoroutine("协程方法名");
//等待WWW操作完成后再执行后续代码
yield return WWW();
//当游戏对象被获取到之后执行
yield return GameObject;
//等待0.3秒,一段指定的时间延迟之后继续执行,在所有的Update函数完成调用的那一帧之后(这里的时间不受到Time.timeScale的影响)
yield return new WaitForSecondsRealtime(0.3f);
//等到下一个固定帧数更新
yield return new WaitForFixedUpdate();
//等到所有相机画面被渲染完毕后更新
yield return new WaitForEndOfFrame();
//跳出协程对应方法,其后面的代码不会被执行
yield break;
//将协同执行直到 当输入的参数(或者委托)为true的时候.... 如:yield return new WaitUntil(() => frame >= 10)。
yield return new WaitUntil();
//将协同执行直到 当输入的参数(或者委托)为false的时候.... 如:yield return new WaitWhile(() => frame < 10)。
yield return new WaitWhile();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 优点
让原来要使用异步 + 回调方式写的非人类代码, 可以用看似同步的方式写出来。能够分步做一个比较耗时的事情,如果需要大量的计算,将计算放到一个随时间进行的协程来处理,能分散计算压力。优点总结如下:
- 程序分帧执行
- 提高运行效率
# 缺点
协程本质是迭代器,且是基于 Unity3d 生命周期的,大量开启协程会引起GC。如果同时激活的协程较多,就可能会出现多个高开销的协程挤在同一帧执行导致的卡帧。
# 协程结束的标志
如果最后一个 yield return 的 IEnumerator 已经迭代到最后一个时,MoveNext 就会返回 false。这时,Unity3d 就会将这个 IEnumerator 从 Cortoutines List 中移除。只有当这个对象的 MoveNext 返回 false 时,即该 IEnumertator 的 Current 已经迭代到最后一个元素了,才会执行 yield return 后面的语句。
# 协程执行顺序
开始协程 -> 执行协程 -> 遇到中断指令中断协程 -> 返回上层函数继续执行上层函数的下一行代码 -> 中断指令结束后 -> 继续执行中断指令之后的代码 -> 协程结束。
# 协程嵌套
协程可以嵌套吗?可以的,yield return StartCoroutine 就是,执行顺序是:子协程中断后,会返回父协程,父协程暂停,返回父协程的上级函数。决定父协程结束的标志是子协程是否结束,当子协程结束后返回父协程执行其后的代码才算结束。
# 协程执行测试
CoroutineTest.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoroutineTest : MonoBehaviour
{
private IEnumerator coroutine;
void Start()
{
print("Start" + Time.time + " Sceonds!");
coroutine = WaitAndPrint();
StartCoroutine(coroutine);
print("协程启动!");
}
private void Update()
{
print("Update " + Time.time + " Seconds!");
}
private void LateUpdate()
{
print("LateUpdate " + Time.time + " Seconds!");
}
private IEnumerator WaitAndPrint()
{
print("尚未挂起!");
yield return null;
print("协程结束:" + Time.time + " Seconds!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
输出结果
Start 0 Sceonds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:Start () (at Assets/Scripts/CoroutineTest.cs:13)
尚未挂起!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest/<WaitAndPrint>d__4:MoveNext () (at Assets/Scripts/CoroutineTest.cs:31)
UnityEngine.MonoBehaviour:StartCoroutine (System.Collections.IEnumerator)
CoroutineTest:Start () (at Assets/Scripts/CoroutineTest.cs:15)
协程启动!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:Start () (at Assets/Scripts/CoroutineTest.cs:16)
Update 0 Seconds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:Update () (at Assets/Scripts/CoroutineTest.cs:21)
LateUpdate 0 Seconds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:LateUpdate () (at Assets/Scripts/CoroutineTest.cs:26)
Update 0.02 Seconds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:Update () (at Assets/Scripts/CoroutineTest.cs:21)
协程结束:0.02 Seconds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest/<WaitAndPrint>d__4:MoveNext () (at Assets/Scripts/CoroutineTest.cs:33)
UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)
LateUpdate 0.02 Seconds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:LateUpdate () (at Assets/Scripts/CoroutineTest.cs:26)
Update 0.12 Seconds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:Update () (at Assets/Scripts/CoroutineTest.cs:21)
LateUpdate 0.12 Seconds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:LateUpdate () (at Assets/Scripts/CoroutineTest.cs:26)
Update 0.172538 Seconds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:Update () (at Assets/Scripts/CoroutineTest.cs:21)
LateUpdate 0.172538 Seconds!
UnityEngine.MonoBehaviour:print (object)
CoroutineTest:LateUpdate () (at Assets/Scripts/CoroutineTest.cs:26)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Game.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
class ClassWaitForTime {
private float total;
private float now;
public ClassWaitForTime(float time) {
this.total = time;
this.now = 0;
}
public void Update(float dt) {
this.now += dt;
}
public bool isOver() {
return (this.now >= this.total);
}
}
public class Game : MonoBehaviour {
private IEnumerator e = null;
IEnumerator start_Coroutine() {
Debug.Log("1111111111");
yield return 1;
Debug.Log("Hellword");
yield return "abc";
Debug.Log("我要睡5秒,别叫醒我");
yield return new ClassWaitForTime(5);
Debug.Log("我醒了");
yield break; // 终止结束了;
}
// Use this for initialization
void Start () {
this.e = this.start_Coroutine();
#if TEST
// step1 创建一个IEnumerator
IEnumerator e = this.temp_Coroutine();
// step2: 启动一个协程
this.StartCoroutine(e);
#endif
}
IEnumerator temp_Coroutine() {
for (int i = 0; i < 10; i++) {
Debug.Log("i = " + i);
// yield return null; // 终止协程,只到下一个帧开始;
yield return new WaitForSeconds(2); // 终止协程,只到2秒结束以后再继续执行
}
}
// Update is called once per frame
void Update () {
}
void LateUpdate() {
if (this.e != null) {
if (this.e.Current is ClassWaitForTime) {
ClassWaitForTime wt = this.e.Current as ClassWaitForTime;
wt.Update(Time.deltaTime);
if (!wt.isOver()) {
return; // 如果时间没有到,你就不要往后面执行了;
}
}
if (!this.e.MoveNext()) {
this.e = null;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
输出结果
1111111111
UnityEngine.Debug:Log (object)
Game/<start_Coroutine>d__1:MoveNext () (at Assets/Scripts/Game.cs:26)
Game:LateUpdate () (at Assets/Scripts/Game.cs:78)
Hellword
UnityEngine.Debug:Log (object)
Game/<start_Coroutine>d__1:MoveNext () (at Assets/Scripts/Game.cs:29)
Game:LateUpdate () (at Assets/Scripts/Game.cs:78)
我要睡5秒,别叫醒我
UnityEngine.Debug:Log (object)
Game/<start_Coroutine>d__1:MoveNext () (at Assets/Scripts/Game.cs:32)
Game:LateUpdate () (at Assets/Scripts/Game.cs:78)
我醒了
UnityEngine.Debug:Log (object)
Game/<start_Coroutine>d__1:MoveNext () (at Assets/Scripts/Game.cs:34)
Game:LateUpdate () (at Assets/Scripts/Game.cs:78)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 协程执行动画
CoroutineExample.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoroutineExample : MonoBehaviour
{
private IEnumerator coroutine;
[SerializeField]
private Transform target;
[SerializeField]
private float smoothing;
// Start is called before the first frame update
void Start()
{
print("Start" + Time.time + " Sceonds!");
coroutine = WaitAndPrint();
StartCoroutine(coroutine);
print("协程启动!");
}
private IEnumerator WaitAndPrint()
{
while (Vector3.Distance(transform.position, target.position) > 0.05f)
{
transform.position = Vector3.Lerp(transform.position, target.position, smoothing * Time.deltaTime);
yield return null;
}
print("移动到目标点!" + Time.time + " Sceonds!");
yield return new WaitForSeconds(3f);
print("协程结束!" + Time.time + " Sceonds!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
输出结果
Start 0 Sceonds!
UnityEngine.MonoBehaviour:print (object)
CoroutineExample:Start () (at Assets/Scripts/CoroutineExample.cs:17)
协程启动!
UnityEngine.MonoBehaviour:print (object)
CoroutineExample:Start () (at Assets/Scripts/CoroutineExample.cs:20)
移动到目标点! 0.1861269 Sceonds!
UnityEngine.MonoBehaviour:print (object)
CoroutineExample/<WaitAndPrint>d__4:MoveNext () (at Assets/Scripts/CoroutineExample.cs:31)
UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)
协程结束! 3.18647 Sceonds!
UnityEngine.MonoBehaviour:print (object)
CoroutineExample/<WaitAndPrint>d__4:MoveNext () (at Assets/Scripts/CoroutineExample.cs:35)
UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# C#迭代器和yield关键字
在 C# 中,IEnumerator 和 IEnumerable 是用于迭代集合的接口,而 yield 是一个关键字,用于简化自定义迭代器的创建。这些特性使得你可以轻松地创建和遍历集合,而无需手动管理迭代过程。
IEnumerator 可以理解为一个容器,里面装了好多个函数。依次执行每个函数,每个函数都有一个返回值,放在 IEnumerator.Current 对象里面。yield关键字会把相关代码和 return 封装成一个函数。MoveNext 是执行当前的函数,将返回值存放到 IEnumerator.Current 并把位置拨动到下一个位置(如果存在)。
相信好多程序员都是因为 Unity3d 的协程才认识 yield 这个关键字的,其实 yield 是 C# 的关键字,Unity3d 的协程只是在 C# 的基础上做了一层封装,我们现在来看看 yield 这个关键字。说到 yield 就不得不说迭代器,迭代器模式是设计模式的一种,因为其运用的普遍性,很多语言都有内嵌的原生支持。在 .NET 中,迭代器模式是通过 IEnumerator、IEnumerable 两个接口和两个同名的泛型接口来封装的:
1. IEnumerable 接口
IEnumerable 是一个接口,它代表一个可枚举的集合。实现了 IEnumerable 的类可以被 foreach 循环遍历。IEnumerable 很简单,直接返回迭代器。
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
2
3
4
IEnumerable 接口要求实现 GetEnumerator 方法,返回一个 IEnumerator 对象,后者可以用于迭代集合。
IEnumerable 允许你通过 foreach 循环访问对象集合。
2. IEnumerator 接口
IEnumerator 是与 IEnumerable 配套的接口,用于在集合中进行迭代。它定义了如何访问集合中的元素并提供了迭代器的核心方法。
IEnumerator 只定义了一个属性、两个函数。Current 为迭代器的当前值。通过调用 MoveNext 函数让迭代器前进一步,返回值表示该迭代器是否结束。Reset 函数用于重置数据。
public interface IEnumerator
{
bool MoveNext(); // 移动到下一个元素
void Reset(); // 重置迭代器
object Current { get; } // 获取当前元素
}
2
3
4
5
6
7
MoveNext:将迭代器移动到集合的下一个元素。如果成功,它返回 true;如果没有更多元素,返回 false。
Reset:将迭代器的位置重置到集合的起始位置(通常不常用)。
Current:返回集合中当前位置的元素。
3. yield 关键字
yield
是一个非常强大的关键字,用于在迭代器中简化元素的返回。通过 yield,你可以在方法中逐个返回元素,而不需要手动管理迭代器的状态。
当使用 yield 时,C# 会自动为你生成一个迭代器类,简化代码并提高可读性。
public class MyEnumerable
{
public IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
}
class Program
{
static void Main()
{
MyEnumerable numbers = new MyEnumerable();
foreach (var number in numbers.GetNumbers())
{
Console.WriteLine(number);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在这个示例中,GetNumbers 方法返回一个 IEnumerable<int>
,并使用 yield return 逐步返回每个元素。每次迭代时,方法会从上次暂停的地方继续执行。
4. yield 的工作原理
当方法执行时,遇到 yield return,它会返回一个值并暂停方法的执行。
下次调用时,执行从上次暂停的地方继续,直到遇到下一个 yield return 或结束。
这种方式非常高效,因为它避免了一次性将所有元素加载到内存中,可以实现懒加载(lazy loading)。
- 在遇到 yield break 或者返回 IEnumerator 的函数体结束前,不管 yield return 的值为多少,MoveNext 都是会返回 True。
- 在第一次调用 MoveNext 之前,返回 IEnumerable 的代码都不会执行,即使你有主动去调用它。
- 执行到 yield return 的地方,代码就暂停了,并返回相应的值,在下一次调用 MoveNext 时,从上次暂停的地方继续执行。
- yield return 代码不能放入 try...catch 块中,但是能放入 try...finally 块中。
5. IEnumerable 和 IEnumerator 的自定义实现
如果你希望自己实现一个迭代器类,也可以手动实现 IEnumerable 和 IEnumerator 接口。但使用 yield 可以简化这个过程,避免你编写复杂的状态管理代码。
public class MyCollection : IEnumerable<int>
{
private int[] _numbers = { 1, 2, 3, 4, 5 };
public IEnumerator<int> GetEnumerator()
{
// 传统的迭代器实现
foreach (var number in _numbers)
{
yield return number;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator(); // 提供非泛型接口的实现
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用 foreach 遍历
class Program
{
static void Main()
{
MyCollection collection = new MyCollection();
foreach (var num in collection)
{
Console.WriteLine(num);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
6. yield 的优势
简化代码:你不需要手动管理迭代器的状态(例如,MoveNext 和 Current),yield 会自动生成。
延迟加载:yield 支持懒加载(延迟加载),这意味着元素只有在需要时才会被计算出来,减少内存消耗。
可读性强:通过 yield,代码更加简洁和直观。
7. 总结
IEnumerable:定义了一个集合,能够提供一个迭代器(返回 IEnumerator)。
IEnumerator:通过 MoveNext 和 Current 让你在集合中逐个元素进行遍历。
yield:简化了迭代器的实现,可以逐步返回元素,避免手动管理迭代器状态,且支持懒加载。
yield 是创建高效、简洁的迭代器的强大工具。如果你有大量数据或需要延迟计算的数据集,使用 yield 会非常方便。