第12章中,咱们看到能够用foreach语句遍历数组。在本章,咱们会进一步探讨数组,来看看为何它们能够被foreach语句处理。咱们还会研究如何使用迭代器为用户自定义类增长该功能。数组
数组foreach语句为咱们依次取出数组中的每一个元素。安全
int[] arr1={10,11,12,13}; foreach(int item in arr1) { Console.WriteLine("Item value: {0}",item); }
为何数组能够这么作?由于数组能够按需提供一个叫作枚举器(enumerator)的对象。枚举器能够依次返回请求的数组中的元素。枚举器“知道”项的次序而且跟踪它在序列中的位置,而后返回请求的当前项。
对于由枚举器的类型,必须有一个方法来获取它。获取对象枚举器的方法是调用对象的GetEnumerator方法。实现GetEnumerator方法的类型叫作可枚举类型(enumerable type或enumerable)。数组是可枚举类型。
函数
foreach结构设计用来和可枚举类型一块儿使用。只要给它的遍历对象是可枚举类型,它就会执行以下行为:编码
实现了IEnumerator接口的枚举器包含3个函数成员:Current、MoveNext、Reset。spa
枚举器与序列中的当前项保持联系的方式彻底取决于实现。能够经过对象引用、索引值或其余方式来实现。对于内置的一维数组来讲,就是用项的索引。
设计
有了集合的枚举器,咱们就可使用MoveNext和Current成员来模仿foreach循环遍历集合中的项。
例:手动作foreach语句自动作的事情。3d
class Program { static void Main() { int[] MyArray={10,11,12,13}; IEnumerator ie=MyArray.GetEnumerator(); while(ie.MoveNext()) { int i=(int)ie.Current; Console.WriteLine("{0}",i); } } }
IEnumerator接口
可枚举类是指实现了IEnumerator接口的类。IEnumerator接口只有一个成员–GetEnumerator方法,它返回对象的枚举器。code
下面是个可枚举类的完整示例,类名Spectrum,枚举器类为ColorEnumerator。对象
using System; using System.Collections; class ColorEnumerator:IEnumerator { string[] _colors; int _position=-1; public ColorEnumerator(string[] theColors) { _colors=new string[theColors.Length]; for(int i=0;i<theColors.Length;i++) { _colors[i]=theColors[i]; } } public object Current { get { if(_position==-1||_position>=_colors.Length) { throw new InvalidOperationException(); } return _colors[_position]; } } public bool MoveNext() { if(_position<_colors.Length-1) { _position++; return true; } else { return false; } } public void Reset() { _position=-1; } } class Spectrum:IEnumerable { string[] Colors={"violet","blue","cyan","green","yellow","orange","red"}; public IEnumerator GetEnumerator() { return new ColorEnumerator(Colors); } } class Program { static void Main() { var spectrum=new Spectrum(); foreach(string color in spectrum) { Console.WriteLine(color); } } }
目前咱们描述的枚举接口都是非泛型版本。实际上,在大多数状况下你应该使用泛型版本IEnumerable<T>
和IEnumerator<T>
。它们叫作泛型是由于使用了C#泛型(参见第17章),其使用方法和非泛型形式差很少。
二者间的本质差异以下:blog
IEnumerable<T>
接口的GetEnumerator方法返回实现IEnumator<T>
的枚举器类的实例IEnumerator<T>
的类实现了Current属性,它返回实际类型的对象,而不是object基类的引用须要重点注意的是,咱们目前所看到的非泛型接口的实现不是类型安全的。它们返回object类型的引用,而后必须转化为实际类型。
而泛型接口的枚举器是类型安全的,它返回实际类型的引用。若是要建立本身的可枚举类,应该实现这些泛型接口。非泛型版本可用于C#2.0之前没有泛型的遗留代码。
尽管泛型版本和非泛型版本同样简单易用,但其结构略显复杂。
可枚举类和枚举器在.NET集合类中被普遍使用,因此熟悉它们如何工做很重要。不过,虽然咱们已经知道如何建立本身的可枚举类和枚举器了,但咱们仍是很高兴听到,C#从2.0版本开始提供了更简单的建立枚举器和可枚举类型的方式。
实际上,编译器将为咱们建立它们。这种结构叫作迭代器(iterator)。咱们能够把手动编码的可枚举类型和枚举器替换为由迭代器生成的可枚举类型和枚举器。
在解释细节前,咱们先看两个示例。下面的方法实现了一饿产生和返回枚举器的迭代器。
public IEnumerator<string>BlackAndWhite() { yield return "black"; yield return "gray"; yield return "white"; }
下面方法声明了另外一个版本,并输出相同结果:
public IEnumerator<string>BlackAndWhite() { string[] theColors={"black","gray","white"}; for(int i=0;i<theColors.Length;i++) { yield return theColors[i]; } }
迭代器块是有一个或多个yield语句的代码块。下面3种类型的代码块中的任意一种均可以是迭代器块:
迭代器块与其余代码块不一样。其余块包含的语句被当作命令式。即先执行代码块中的第一个语句,而后执行后面的语句,最后控制离开块。
另外一方面,迭代器块不是须要在同一时间执行的一串命令式命令,而是描述了但愿编译器为咱们建立的枚举器类的行为。迭代器块中的代码描述了如何枚举元素。
迭代器块由两个特殊语句:
编译器获得有关枚举项的描述后,使用它来构建包含全部须要的方法和属性实现的枚举器类。结果类被嵌套包含在迭代器声明的类中。
以下图所示,根据迭代器块的返回类型,你可让迭代器产生枚举器或可枚举类型。
class MyClass { public IEnumerator<string> GetEnumerator() { return BlackAndWhite(); //返回枚举器 } public IEnumerator<string> BlackAndWhite()//迭代器 { yield return "black"; yield return "gray"; yield return "white"; } } class Program { static void Main() { var mc=new MyClass(); foreach(string shade in mc) { Console.WriteLine(shade); } } }
下图演示了MyClass的代码及产生的对象。注意编译器为咱们自动作了多少工做。
IEnumerator<string>
IEnumerator<string>
以前示例建立的类包含两部分:产生返回枚举器方法的迭代器以及返回枚举器的GetEnumerator方法。
本节例子中,咱们用迭代器来建立可枚举类型,而不是枚举器。与以前的示例相比,本例有如下不一样:
IEnumerable<string>
而不是IEnumerator<string>
。所以MyClass首先调用BlackAndWhite方法获取它的可枚举类型对象,而后调用对象的GetEnumerator方法来获取结果,从而实现GetEnumerator方法class MyClass { public IEnumerator<string> GetEnumerator() { IEnumerable<string> myEnumerable=BlackAndWhite(); return myEnumerable.GetEnumerator(); } public IEnumerable<string> BlackAndWhite()//迭代器 { yield return "black"; yield return "gray"; yield return "white"; } } class Program { static void Main() { var mc=new MyClass(); foreach(string shade in mc) { Console.Write(shade); } foreach(string shade in mc.BlackAndWhite) { Console.Write(shade); } } }
下图演示了在代码的可枚举迭代器产生泛型可枚举类型。
IEnumerable<string>
IEnumerator<string>
和IEnumerable<string>
前面两节展现了,咱们能够建立迭代器来返回可枚举类型或枚举器。下图总结了如何使用普通迭代器模式。
下例中,Spectrum类有两个可枚举类型的迭代器。注意尽管它有两个方法返回可枚举类型,但类自己不是可枚举类型,由于它没有实现GetEnumerator
using System; using System.Collections.Generic; class Spectrum { string[] colors={"violet","blue","cyan","green","yellow","orange","red"}; public IEnumerable<string> UVtoIR() { for(int i=0;i<colors.Length;i++) { yield return colors[i]; } } public IEnumerable<string> IRtoUV() { for(int i=colors.Length-1;i>=0;i--) { yield return colors[i]; } } } class Program { static void Main() { var spectrum=new Spectrum(); foreach(string color in spectrum.UVtoIR()) { Console.Write(color); } Console.WriteLine(); foreach(string color in spectrum.IRtoUV()) { Console.Write(color); } Console.WriteLine(); } }
本例演示两个内容:第一,使用迭代器来产生具备两个枚举器的类;第二,演示迭代器如何实现属性。
using System; using System.Collections.Generic; class Spectrum { bool _listFromUVtoIR; string[] colors={"violet","blue","cyan","green","yellow","orange","red"}; public Spectrum(bool listFromUVtoIR) { _listFromUVtoIR=listFromUVtoIR; } public IEnumerator<string> GetEnumerator() { return _listFromUVtoIR?UVtoIR:IRtoUV; } public IEnumera<string> UVtoIR { get { for(int i=0;i<colors.Length;i++) { yield return colors[i]; } } } public IEnumerable<string> IRtoUV { get { for(int i=colors.Length-1;i>=0;i--) { yield return colors[i]; } } } } class Program { static void Main() { var startUV=new Spectrum(true); var startIR=new Spectrum(false); foreach(string color in startUV) { Console.Write(color); } Console.WriteLine(); foreach(string color in startIR) { Console.Write(color); } Console.WriteLine(); } }
以下是须要了解的有关迭代器的其余重要事项。
在后台,由编译器生成的枚举器类是包含4个状态的状态机。
若是状态机在Before或Suspended状态时调用MoveNext方法,就转到了Running状态。在Running状态中,它检测集合的下一项并设置为知。
若是有更多项,状态机会转入Suspended状态,若是没有更多项,它转入并保持在After状态。