foreach在数组集合的遍历时会被经常用到,例如:
string[] strs = new string[] { "red", "green","blue" }; foreach (var item in strs) { Console.WriteLine(item); }
使用foreach做遍历确实很方便,然而并不是每一种类型都能使用foreach进行遍历操作,只有实现了IEnumerable接口的类(也叫做可枚举类型)才能进行foreach的遍历操作,集合和数组已经实现了这个接口,所以能进行foreach的遍历操作
IEnumerable叫做可枚举接口,它的成员只有一个
- GetEnumerator()
返回一个枚举器对象,即实现了IEnumerator接口的类的实例,实现IEnumerator接口的枚举器包含3个函数成员:
- Current属性
- MoveNext()方法
- Reset()方法
Current属性为只读属性,返回枚举序列中的当前位置,MoveNext()把枚举器的位置前进到下一项,返回布尔值,新的位置若是有效的,返回true,否则返回false,ReSet()将位置重置为原始状态。
举个例子,实现自己的可枚举类型,先实现IEnumerator枚举器类型:class Enumerator1
: IEnumerator { 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; } } 这里实现的是IEnumerator泛型枚举器类,泛型枚举器类的实现中包含一个非泛型的Current属性,返回Object对象的引用,枚举器开始位置的状态为第一个元素之前,_position为-1,因此当我们取到Current之前,应先调用一次MoveNext(),才能得到可枚举项中的第一项,当位置到达可枚举项的最后一项,再次调用MoveNext(),会返回false,取值过程终止,除非位置被重置。
再来实现IEnumerable枚举类型:class Enumeratable1
: IEnumerable { 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 GetEnumerator() { return new Enumerator1 (t); } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator1 (t); } }` 这里实现的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
strEnum = new Enumeratable1 (strs); Enumeratable1 numEnum = new Enumeratable1 (nums); foreach (var temp in strEnum) { Console.WriteLine(temp); } Console.ReadKey(); } 输出结果:
在实现MoveNext()方法时,添加了一行输出,从输出结果可以看出,用foreach做遍历时,先调用了MoveNext()方法,再调用Current属性获得当前项,当取到最后一项时,并没有退出枚举过程,而是再次调用MoveNext()方法,返回值为false后,才退出枚举过程。
当我们自己实现可枚举类型,在foreach中完成遍历时,需要手动实现IEnumerable枚举接口和IEnumerator枚举器接口。C#2.0以后提供了更为简单的方式去创建可枚举类型和枚举器,那就是迭代器,迭代器会生成枚举类型和枚举器类型。
举个栗子:public IEnumerator
Color() //迭代器 { yield return "red"; yield return "green"; yield return "blue"; } 以上代码就是一个迭代器创建枚举器的过程,如果手动创建枚举器,是需要实现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
GetEnumerator() { return Color(); } public IEnumerator Color() { yield return "red"; yield return "green"; yield return "blue"; }} ColorEnumeable类实现了GetEnumerator()方法,为可枚举类型。迭代器可以返回一个枚举器类型,也可以返回一个可枚举类型,例如:
public IEnumerable
Color() { yield return "red2"; yield return "green2"; yield return "blue2"; } 以上代码是利用迭代器返回一个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
Color() { yield return "red2"; yield return "green2"; yield return "blue2"; } } 在foreach的语句中用的是color2.Color(),而不是像上一个例子中直接使用的color,这是因为ColorEnumerable类中已经公开实现了GetEnumerator()方法,但在ColorEnumerable2类中并没有公开实现,所以不能使用foreach直接遍历ColorEnumerable2类的实例,但ColorEnumerable2的方法Color()使用迭代器创建了可枚举类,已经实现了GetEnumerator()方法,所以使用color2.Color()可以得到可枚举类在foreach中进行遍历。我们对ColorEnumerable2作如下修改:
class ColorEnumerable2 { public IEnumerator
GetEnumerator() { return Color().GetEnumerator(); } public IEnumerable Color() { yield return "red2"; yield return "green2"; yield return "blue2"; } } 便可以在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
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 ie = Enumerable(); IEnumerator 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(); } } 为了调用枚举器中的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); }
输出结果:
在迭代器的foreach循环结束后使用yield break语句,yield break 语句后的迭代过程并没有进行,而是把MoveNext()的返回值设置为false,调用MoveNext()方法后进入到finally中,结束迭代过程。关于迭代器的退出:
- 在正常迭代进行到最后一项时,迭代没有结束MoveNext()的值被设置为false,再次调用MoveNext(),结束迭代过程
- 遇到yield break语句,MoveNext()的值被设置为false,再次调用MoveNext(),结束迭代过程
- yield return语句只是让迭代器状态暂停挂起,等待下一次的MoveNext()调用,继续进行yield之后的语句
使用过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"); }
在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,你究竟干了什么这篇博客。
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); }
添加了一行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"); }
再来看看输出结果:
输出结果是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);就立即终止,而是会离开迭代器之前将离开前的代码执行完。
- Coroutine,你究竟干了什么
- Unity协程(Coroutine)原理深入剖析