C#图解教程之枚举器和迭代器

作者 Zhendong Ho 日期 2019-08-25
C#
C#图解教程之枚举器和迭代器

枚举器和可枚举类型

我们都知道,可以用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 // 实现IEnumerable接口
{
public IEnumerator GetEnumerator() // 返回IEnumerator类型的对象
{
// ...
}
}

使用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 // 实现Current
{
get
{
if (_position == -1)
{
throw new InvalidOperationException();
}
if (_position >= _colors.Length)
{
throw new InvalidOperationException();
}
return _colors[_position];
}
}

public bool MoveNext() // 实现MoveNext
{
if (_position < _colors.Length - 1)
{
_position++;
return true;
}
else
{
return false;
}
}

public void Reset() // 实现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) // 使用MyClass的实例
{
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,让类不可枚举。
    1. 如果实现GetEnumerator,让它调用迭代器方法以获取自动生成的实现IEnumerable的类实例。然后,从IEnumerable对象返回由GetEnumerator创建的枚举器,如右边部分。
    2. 如果通过不实现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状态。