枚举器和可枚举类型
我们都知道,可以用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结构设计用来和可枚举类型一起使用。只要给它的遍历对象是可枚举类型,比如数组,它就会执行如下行为:
- 通过调用GetEnumerator方法获取对象的枚举器。
- 从枚举器中请求每一项并且把它作为迭代变量(iteration variable),代码可以读取该变量但不可以改变。
IEnumerator接口
实现了IEnumerator接口的枚举器包含3个函数成员:Current、MoveNext以及Reset。
Current:返回序列中当前位置项的属性。
- 它是只读属性。
- 它返回object类型的引用,所以可以返回任何类型。
MoveNext:把枚举器位置向前到集合中的下一项的方法。
- 它也返回布尔值,指示新的位置是有效位置还是已经超过了序列的尾部。
- 如果新的位置是有效的,方法返回true。
- 如果新的位置是无效的(比如当前位置到达了尾部),方法返回false。
- 枚举器的原始位置在序列中的第一项之前,因此MoveNext必须在第一次使用Current之前调用。
Reset:把位置重置为原始状态的方法。
注意:枚举器与序列中的当前项保持联系的方式完全取决于实现。可以通过对象引用、索引值或其他方式实现。对于内置的一维数组来说,就使用项的索引。
有了集合的枚举器,我们就可以使用MoveNext和Current成员来模仿foreach循环遍历集合中的项。
例:手动做foreach语句自动做的事情。
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); } } }
|
IEnumerable接口
可枚举类是指实现了IEnumerable接口的类。IEnumerable接口只有一个成员,就是GetEnumerator方法,它返回对象的枚举器。
using System.Collections;
class MyClass : IEnumerable { public IEnumerator GetEnumerator() { } }
|
使用IEnumerable和IEnumerator的实例
using System; using System.Collections;
class ColorEnumerator : IEnumerator { string[] _colors; int _position = -1; public ColorEnumerator(string[] theColors) { _color = new string[theColors.Lengeh]; for (int i = 0; i < theColors.Length; i++) { _colors[i] = theColors[i]; } } public object Current { get { if (_position == -1) { throw new InvalidOperationException(); } if (_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() { _postion = -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() { Spectrum spectrum = new Spectrum(); foreach (string color in spectrum) { Console.WriteLine(color); } } }
|
输出结果:
泛型枚举接口
目前我们描述的枚举接口都是非泛型版本的。实际上,在大多数情况下应该使用泛型版本IEnumerable<T>和IEnumerator<T>。区别如下。
对于非泛型接口形式:
- IEnumerable接口的GetEnumerator方法返回实现IEnumerator枚举器类的实例。
- 实现IEnumerator的类实现了Current属性,它返回object类型的引用,然后我们必须把它转化为实际类型的对象。
对于泛型接口形式:
- IEnumerable<T>接口的GetEnumerator方法返回实现IEnumerator<T>的枚举器类的实例。
- 实现IEnumerator<T>的类实现了Current属性,它返回实际类型的对象,而不是object基类的引用。
注意:我们目前所看到的非泛型接口的实现不是类型安全的。它们返回object类型的引用,然后必须转化为实际类型。而泛型接口的枚举器是类型安全的,它返回实际类型的引用。
迭代器
可枚举类和枚举器在.NET集合类中被广泛使用,所以熟悉它们如何工作很重要。不过C#从2.0版本开始,提供了更简单的创建枚举器和可枚举类型的的方式(编译器自动帮我们创建)。这种结构叫做迭代器(iterator)。
下面的方法实现了一个产生和返回枚举器的迭代器。
public IEnumerator<string> BlackAndWhite() { yield return "black"; yield return "gary"; yield return "white"; }
|
另一个版本。
public IEnumerator<string> BlackAndWhite() { string[] theColors = { "black", "gary", "white" }; for (int i = 0; i < theColors.Length; i++) { yield return thrColors[i]; } }
|
yield是C#为了简化遍历操作实现的语法糖,代替了某个类型实现IEnumerable接口的方式。
迭代器块
迭代器块是一个或多个yield语句的代码块。下面3种类型的代码中的任意一种都可以是迭代快:
迭代器块有两个特殊语句。
yield return:指定了序列中返回的下一项。
yield break:指定在序列中没有的其他项。
编译器得到有关枚举项的描述后,使用它来构建包含有需要的方法和属性实现的枚举器类。结果类被嵌套包含在迭代器声明的类中。
如下图所示,根据迭代器的返回类型,可以让迭代器产生枚举器或可枚举类型。
使用迭代器来创建枚举器
class MyClass { public IEnumerator<string> GetEnumerator() { return BlackAndWhite(); } public IEnumerator<string> BlackAndWhite() { yield return "black"; yield return "gary"; yield return "white"; } }
class Program { static void Main() { MyClass mc = new MyClass(); foreach (string shade in mc) { Console.WriteLine(shade); } } }
|
输出结果:
下图左边演示了MyClass的代码,右边演示了产生的对象。
- 左边的迭代器代码演示了它的返回类型是IEnumerator<string>。
- 右边演示了它有一个嵌套类实现了IEnumerator<string>。
使用迭代器来创建可枚举类型
class MyClass { public IEnumerator<string> GetEnumerator() { public IEnumerator<string> myEnumerable = BlackAndWhite(); return myEnumerable.GetEnumerator(); } public IEnumerable<string> BlackAndWhite() { yield return "black"; yield return "gary"; yield return "white"; } }
class Program { static void Main() { MyClass mc = new MyClass(); foreach (string shade in mc) { Console.Write("{0} ", shade); } foreach (string shade in mc.BlackAndWhite()) { Console.Write("{0} ", shade); } } }
|
输出结果:
下图演示了在代码中的可枚举迭代器产生了泛型可枚举类型。
- 左边的迭代器代码演示了它的返回类型是IEnumerable<string>。
- 右边演示了它有一个嵌套类实现了IEnumerator<string>和IEnumerable<string>。
常见迭代器模式
我们可以创建迭代器来返回可枚举类型或枚举器。下图总结了如何使用普通迭代器模式。
- 当我们实现返回枚举器的迭代器时,必须通过实现GetEnumerator来让类可枚举,它返回由迭代器返回的枚举器,如左边部分。
- 如果我们在类中实现迭代器返回可枚举类型,可以让类实现GetEnumerator来让类本身可被枚举,或者不实现GetEnumerator,让类不可枚举。
- 如果实现GetEnumerator,让它调用迭代器方法以获取自动生成的实现IEnumerable的类实例。然后,从IEnumerable对象返回由GetEnumerator创建的枚举器,如右边部分。
- 如果通过不实现GetEnumerator使类本身不可枚举,仍然可以使用迭代器返回的可枚举类,只需要直接调用迭代器方法。如右边的第二个foreach语句所示。
产生多个可枚举类型
下例中,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() { Spectrum spectrum = new Spectrum(); foreach (string color in spectrum.UVToIR()) { Console.Write("{0} ", color); } Console.WriteLine(); foreach (string color in spectrum.IRToUV()) { Console.Write("{0} ", 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 IEnumerator<string> UVToIR { get { for (int i = 0; i < colors.Length; i++) { yield return colors[i]; } } } public IEnumerator<string> IRToUV { get { for (int i = colors.Length - 1; i >= 0; i--) { yield return colors[i]; } } } }
class Program { static void Main() { Spectrum startUV = new Spectrum(true); Spectrum startIR = new Spectrum(false); foreach (string color in startUV) { Console.Write("{0} ", color); } Console.WriteLine(); foreach (string color in startIR) { Console.Write("{0} ", color); } Console.WriteLine(); } }
|
输出结果:
迭代器实质
- 迭代器(不一定)需要System.Collections.Generic命名空间,因此我们需要使用using指令引入它。
- 在编译器生成的枚举器中,Reset方法没有实现。而它是接口需要的方法,因此调用时总是抛出System.NotSupportedException异常。
在后台,由编译器生成的枚举器类是包含4个状态的状态机。
Before:首次调用MoveNext的初始状态。
Running:调用MoveNext后进入这个状态。在这个状态中,枚举器检测并设置下一项的位置。在遇到yield return、yield break或在迭代器体结束时,退出状态。
Suspended:状态机等待下次调用MoveNext的状态。
After:没有更多项可以枚举。
如果状态机在Before或Suspended状态时调用了MoveNext方法,就转到了Running状态。在Running状态中,它检测集合的下一项并且设置位置。
如果有更多项,状态机会转入Suspended状态,如果没有更多项,它转入并保持在After状态。