首页 > 技术文章 > C#迭代器——由foreach说开去

liuslayer 2019-08-28 10:59 原文

原文链接:https://blog.csdn.net/u013477973/article/details/65635737

 

C#迭代器——由foreach说开去
foreach在数组集合的遍历时会被经常用到,例如:

string[] strs = new string[] { "red", "green","blue" };
foreach (var item in strs)
{
Console.WriteLine(item);
}
1
2
3
4
5
使用foreach做遍历确实很方便,然而并不是每一种类型都能使用foreach进行遍历操作,只有实现了IEnumerable接口的类(也叫做可枚举类型)才能进行foreach的遍历操作,集合和数组已经实现了这个接口,所以能进行foreach的遍历操作

IEnumerable和IEnumerator
IEnumerable叫做可枚举接口,它的成员只有一个

GetEnumerator()
返回一个枚举器对象,即实现了IEnumerator接口的类的实例,实现IEnumerator接口的枚举器包含3个函数成员:

Current属性
MoveNext()方法
Reset()方法
Current属性为只读属性,返回枚举序列中的当前位置,MoveNext()把枚举器的位置前进到下一项,返回布尔值,新的位置若是有效的,返回true,否则返回false,ReSet()将位置重置为原始状态。
举个例子,实现自己的可枚举类型,先实现IEnumerator枚举器类型:

class Enumerator1<T> : IEnumerator<T> {
private int _position = -1;
private T[] t;
public Enumerator1(T[] a) {
t = new T[a.Length];
for (int i = 0; i < t.Length; i++) {
t[i] = a[i];
}
}
public T Current
{
get
{
if (_position == -1) {
throw new InvalidOperationException();
}
return t[_position];
}
}
object IEnumerator.Current
{
get
{
if (_position == -1)
{
throw new InvalidOperationException();
}
return t[_position];
}
}
public void Dispose()
{
}
public bool MoveNext()
{
Console.WriteLine("Call Move Next");
if (_position >= t.Length)
return false;
else
{
_position++;
return _position < t.Length;
}
}
public void Reset()
{
_position = -1;
}
}
1
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
这里实现的是IEnumerator泛型枚举器类,泛型枚举器类的实现中包含一个非泛型的Current属性,返回Object对象的引用,枚举器开始位置的状态为第一个元素之前,_position为-1,因此当我们取到Current之前,应先调用一次MoveNext(),才能得到可枚举项中的第一项,当位置到达可枚举项的最后一项,再次调用MoveNext(),会返回false,取值过程终止,除非位置被重置。
再来实现IEnumerable枚举类型:

class Enumeratable1<T> : IEnumerable<T>
{
private T[] t;
public Enumeratable1(T[] a) {
t = new T[a.Length];
for (int i = 0; i < t.Length; i++) {
t[i] = a[i];
}
}
public IEnumerator<T> GetEnumerator()
{
return new Enumerator1<T>(t);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator1<T>(t);
}
}`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这里实现的IEnumerable泛型枚举类,泛型枚举类的实现中包含一个非泛型的GetEnumerator方法,GetEnumerator返回的为之前创建的Enumerator1的泛型构造类的实例,使用泛型数组作为参数传入构造函数,创建Enumerator1的枚举器对象。
下面使用foreach对Enumerable1的实例进行遍历:

static void Main() {
string[] strs = new string[] { "red", "green", "blue" };
int[] nums = new int[] { 1, 2, 3, 4, 5 };
Enumeratable1<string> strEnum = new Enumeratable1<string>(strs);
Enumeratable1<int> numEnum = new Enumeratable1<int>(nums);
foreach (var temp in strEnum)
{
Console.WriteLine(temp);
}
Console.ReadKey();
}
1
2
3
4
5
6
7
8
9
10
11
输出结果:


在实现MoveNext()方法时,添加了一行输出,从输出结果可以看出,用foreach做遍历时,先调用了MoveNext()方法,再调用Current属性获得当前项,当取到最后一项时,并没有退出枚举过程,而是再次调用MoveNext()方法,返回值为false后,才退出枚举过程。

迭代器 和 yield return
当我们自己实现可枚举类型,在foreach中完成遍历时,需要手动实现IEnumerable枚举接口和IEnumerator枚举器接口。C#2.0以后提供了更为简单的方式去创建可枚举类型和枚举器,那就是迭代器,迭代器会生成枚举类型和枚举器类型。
举个栗子:

public IEnumerator<string> Color() //迭代器
{
yield return "red";
yield return "green";
yield return "blue";
}
1
2
3
4
5
6
以上代码就是一个迭代器创建枚举器的过程,如果手动创建枚举器,是需要实现IEnumerator的接口的,而在这里,并没有实现Current,MoveNext(),Reset这些成员,取而代之使用的是yield return语句,简单来说迭代器就是使用一个或多个yield return语句告诉编译器创建枚举器类,yield return语句指定了枚举器中下一个可枚举项,完整的代码:

class Program{
static void Main()
{
ColorEnumerable color = new ColorEnumerable();
foreach (var item in color)
{
Console.WriteLine(item);
}
Console.ReadKey();
}}
class ColorEnumerable {
public IEnumerator<string> GetEnumerator()
{
return Color();
}
public IEnumerator<string> Color()
{
yield return "red";
yield return "green";
yield return "blue";
}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ColorEnumeable类实现了GetEnumerator()方法,为可枚举类型。迭代器可以返回一个枚举器类型,也可以返回一个可枚举类型,例如:

public IEnumerable<string> Color() {
yield return "red2";
yield return "green2";
yield return "blue2";
}
1
2
3
4
5
以上代码是利用迭代器返回一个IEnumerable的可枚举类型,完整的代码如下:

class Program{
static void Main()
{
ColorEnumerable2 color2 = new ColorEnumerable2();
foreach (var item in color2.Color())
{
Console.WriteLine(item);
}
Console.ReadKey();
}}
class ColorEnumerable2 {
public IEnumerable<string> Color() {
yield return "red2";
yield return "green2";
yield return "blue2";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在foreach的语句中用的是color2.Color(),而不是像上一个例子中直接使用的color,这是因为ColorEnumerable类中已经公开实现了GetEnumerator()方法,但在ColorEnumerable2类中并没有公开实现,所以不能使用foreach直接遍历ColorEnumerable2类的实例,但ColorEnumerable2的方法Color()使用迭代器创建了可枚举类,已经实现了GetEnumerator()方法,所以使用color2.Color()可以得到可枚举类在foreach中进行遍历。我们对ColorEnumerable2作如下修改:

class ColorEnumerable2 {
public IEnumerator<string> GetEnumerator() {
return Color().GetEnumerator();
}
public IEnumerable<string> Color() {
yield return "red2";
yield return "green2";
yield return "blue2";
}
}
1
2
3
4
5
6
7
8
9
10
便可以在foreach的语句中直接使用ColorEnumerable2的实例进行遍历了。总结一下就是:

yield reutrn可以根据返回类型告诉编译器创建可枚举类或者是枚举器
yield return 语句指定了枚举器对象中的下一个可枚举项
迭代器的执行顺序
当创建完一个可枚举类型(不管是手动实现或是使用迭代器创建),枚举器实际上可以看做是包含4个状态的状态机(参考C#图解教程):

Before 首次调用MoveNext()时的状态,初始位置在第一个可枚举项之前
Running 调用MoveNext后进入该状态。在Running状态下,枚举器检测下一项的位置,当遇到yield return时会进入挂起状态,直到遇到下一个MoveNext(),当遇到yield break时或迭代器结束时,会退出状态
Suspended 暂时挂起状态,等待下一次MoveNext()调用时唤醒
After 已经到最后的位置,没有可枚举项
如下图所示:


通过代码来具体看这个过程:

class Program
{
static readonly String Padding = new String(' ', 35);
static IEnumerable<Int32> Enumerable() {
try {
Console.WriteLine("{0}进入Enumerable方法", Padding);
for (int i = 0; i < 4; i++)
{
Console.WriteLine("{0}yield return {1} 开始",Padding,i);
yield return i;
Console.WriteLine("{0}yield return {1} 结束",Padding,i);
}
Console.WriteLine("{0}最后一个yield return 开始", Padding);
yield return -1;
Console.WriteLine("{0}最后一个yield return 结束", Padding);
}
finally
{
Console.WriteLine("{0}Enumerable方法结束",Padding);
}
}
static void Main(string[] args)
{
IEnumerable<Int32> ie = Enumerable();
IEnumerator<Int32> ietor = ie.GetEnumerator();
Console.WriteLine("开始迭代");
while (true) {
Console.WriteLine("调用MoveNext()之前");
bool result=ietor.MoveNext();
Console.WriteLine("调用MoveNext()之后,值为:{0}", result);
if (!result) {
break;
}
Console.WriteLine("Current值为:{0}",ietor.Current);
}
Console.ReadKey();
}
}
1
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
为了调用枚举器中的MoveNext()方法,使用while循环进行遍历,输出的结果:

从输出结果可以看出:

直到调用MoveNext()后才进入迭代器的方法
遇到yield return,进入暂停挂起状态,,回到开始调用的地方,获取Current,位置被移到下一项
再次遇到MoveNext()方法,暂停挂起的状态回到运行状态,此时再次遇到yield return,重复暂停挂起的过程
当进行到最后一项时,迭代器并没有立即结束,而是又执行了一次MoveNext()方法,此时MoveNext()返回的值为false,执行finally中的代码,退出迭代器
迭代器的退出除了当迭代器状态结束时会发生,使用yield break也会使迭代器退出:

try {
Console.WriteLine("{0}进入Enumerable方法", Padding);
for (int i = 0; i < 4; i++)
{
Console.WriteLine("{0}yield return {1} 开始",Padding,i);
yield return i;
Console.WriteLine("{0}yield return {1} 结束",Padding,i);
}
yield break;
Console.WriteLine("{0}最后一个yield return 开始", Padding);
yield return -1;
Console.WriteLine("{0}最后一个yield return 结束", Padding);
}
finally
{
Console.WriteLine("{0}Enumerable方法结束", Padding);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输出结果:

 

在迭代器的foreach循环结束后使用yield break语句,yield break 语句后的迭代过程并没有进行,而是把MoveNext()的返回值设置为false,调用MoveNext()方法后进入到finally中,结束迭代过程。关于迭代器的退出:

在正常迭代进行到最后一项时,迭代没有结束MoveNext()的值被设置为false,再次调用MoveNext(),结束迭代过程
遇到yield break语句,MoveNext()的值被设置为false,再次调用MoveNext(),结束迭代过程
yield return语句只是让迭代器状态暂停挂起,等待下一次的MoveNext()调用,继续进行yield之后的语句
Unity中的StartCoroutine与yield return
使用过Unity的延时WaitForSeconds()的朋友们一定不会对StartCoroutine和yield return这两个关键字感到陌生。UnityGems.com给出了协程的定义:
A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.
即协程是一个分部执行,遇到条件(yield return 语句)会挂起,直到条件满足才会被唤醒继续执行后面的代码。
先来个例子:

bool isStartCall = false;
bool isUpdateCall = false;
bool isLateUpdateCall = false;
void Start () {
if (!isStartCall) {
Debug.Log("Start Call Begin");
StartCoroutine("StartCoroutines");
Debug.Log("Start Call End");
isStartCall = true;
}
}
IEnumerator StartCoroutines() {
Debug.Log("StartCoroutine Call Begin");
yield return null;
Debug.Log("StartCoroutine Call End");
}
void Update() {
if (!isUpdateCall) {
Debug.Log("Update Call Begin");
StartCoroutine("UpdateCoroutines");
Debug.Log("Update Call End");
isUpdateCall = true;
}
}
IEnumerator UpdateCoroutines()
{
Debug.Log("UpdateCoroutine Call Begin");
yield return null;
Debug.Log("UpdateCoroutine Call End");
}
void LateUpdate() {
if (!isLateUpdateCall) {
Debug.Log("LateUpdate Call Begin");
StartCoroutine("LateUpdateCoroutines");
Debug.Log("LateUpdate Call End");
isLateUpdateCall = true;
}
}
IEnumerator LateUpdateCoroutines()
{
Debug.Log("LateUpdateCoroutine Call Begin");
yield return null;
Debug.Log("LateUpdateCoroutine Call End");
}
1
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
在Start(),Update()和LateUpdate()中分别使用StartCoroutine()方法,传入的参数是一个实现了枚举器的方法名的字符串,枚举器的实现使用的迭代器,通过yield return语句完成,下面是输出结果:

 

输出结果中包含较多的信息,先来看看Start()方法中的StartCoroutine的执行顺序:

输出Start Call Begin
进入StartCoroutines()方法,输出StartCoroutine Call Begin
遇到yield return语句,暂时挂起,回到调用的地方
输出Start Call End
Start()方法执行完毕
当Start()方法执行完毕的时候,程序并没有回到StartCoroutines()内,而是按照上面的过程走完Update(),LataUpdate()方法之后才回到StartCoroutines()内,之后是UpdateCoroutines()和LateUpdateCoroutines(),这是为什么呢?这里涉及到Coroutine的执行顺序,Unity中,Start(),Update(),LateUpdate()依次执行,这没什么好说的,Coroutine的执行是在每一帧的LateUpdate()方法之后,这里需要注意一下,在每个Coroutine的yield return语句中传入的是null,程序会停留一帧,因此是在直到LateUpdate()方法执行完之后才会进入到StartCoroutine()的方法内。
之前我们看到的迭代器退出是在迭代器执行到MoveNext()方法时返回的值为false的情况下才会退出,但是刚才的示例代码中并没有看到MoveNext()的取用,原因在于这个调用和检测过程由Unity完成了:

Unity在每帧调用 协程(迭代器)MoveNext() 方法,如果返回 true ,就从当前位置继续往下执行。
在每个Coroutines里只有一个可列举项,Unity在LateUpdate()执行完后开始调用每个Coroutines中的MoveNext(),(执行一次后MoveNext()的返回值已经为fasle),Coroutines结束暂停挂起,回到运行状态,输出Debug.Log()后,下一帧再次调用MoveNext(),由于已经是false值,Coroutines退出迭代。
Unity中在创建IEnumerator枚举器时,yield return 后面可以有如下表达式:

null - the coroutine executes the next time that it is eligible
WaitForEndOfFrame - the coroutine executes on the frame, after all of the rendering and GUI is complete
WaitForFixedUpdate - causes this coroutine to execute at the next physics step, after all physics is calculated
WaitForSeconds - causes the coroutine not to execute for a given game time period
WWW - waits for a web request to complete (resumes as if WaitForSeconds or null)
Another coroutine - in which case the new coroutine will run to completion before the yielder is resumed
其中有关延时的处理在StartCoroutine()中被特殊处理了,Unity在每一帧调用这几个类的MoveNext之前会先判断延时的条件是否已经满足,如果满足,才会执行后面的MoveNext()操作,如果不满足,会跳出迭代器的方法下一帧再次进行检测,直到满足延时条件,执行MoveNext()操作,当MoveNext()的值为false,退出迭代器。关于被特殊处理的过程,可以参考Coroutine,你究竟干了什么这篇博客。

Coroutine的停止
Unity中Coroutine停止掉的方法,通过将附有脚本的gameObject.SetActive(false)可以停止掉Corotine,就像C#中在迭代器使用yield break一样,不过再重新设置为gameObject.SetActive(true)时,Coroutine并不会恢复。例如,将Update()中的代码修改为:

void Update() {
if (!isUpdateCall) {
Debug.Log("Update Call Begin");
StartCoroutine("UpdateCoroutines");
Debug.Log("Update Call End");
isUpdateCall = true;
gameObject.SetActive(false);
}
1
2
3
4
5
6
7
8
添加了一行gameObject.SetActive(false),再来看一下输出结果:

 

当代码执行到UpdateCoroutines()时,输出”UpdateCoroutine Call Begin”,遇到yield return迭代器进入暂停挂起状态,回到Update()中,输出”Update Call End”,遇到gameObject.SetActive(false),此时开启的Coroutines全部终止,后面的正常方法LateUpdate()和三个Coroutine里面的输出无法进行。
这是gameObject.SetActive(false)放在Coroutine的外部,如果放在内部会是什么情况?修改之前的代码,将gameObject.SetActive(false)放到UpdateCoroutines里面:

IEnumerator UpdateCoroutines()
{
Debug.Log("UpdateCoroutine Call Begin");
yield return null;
gameObject.SetActive(false);
Debug.Log("UpdateCoroutine Call End");
}
1
2
3
4
5
6
7
再来看看输出结果:

 

输出结果是11行,只有最后一个LateUpdateCoroutines中yield return后面的LateUpdateCoroutine Call End没有并执行,通过之前的分析过程,这里的输出结果并不难解释,当代码再次回到UpdateCorotineds的yield return语句之后,前面的输出应该是“StartCoroutines Call End”,这里需要注意的是,尽管 gameObject.SetActive(false)在Debug.Log(“UpdateCoroutine Call End”)语句之前,但是”UpdateCoroutine Call End”还是会输出,也就是说,当gameObject.SetActive(false)在迭代器内部时,代码并不是一走到 gameObject.SetActive(false);就立即终止,而是会离开迭代器之前将离开前的代码执行完。
————————————————
版权声明:本文为CSDN博主「O213」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013477973/article/details/65635737

推荐阅读