枚举器(enumerator)是一个只读的且只能在值序列上前移的游标。在C#中,如果类型满足以下规则中的一种,那么该类型就可以作为枚举器使用:
· 拥有名为 MoveNext 的public无参方法,并拥有一个名为 Current 的public属性。
· 实现了 System.Collections.Generic.IEnumerator<T> 接口。
· 实现了 System.Collections.IEnumerator 接口。
foreach 语句用来在可枚举(enumerable)对象上执行迭代操作。可枚举对象是序列的逻辑表示,它本身不是游标,但是它可以在对象自身上生成游标。在C#中,如果类型满足以下规则中的一种,那么该类型就是一个可枚举类型(以下规则的检查是按顺序执行的):
· 拥有名为 GetEnumerator 的public无参方法,且该方法返回枚举器。
· 实现了 System.Collections.Generic.IEnumerable<T> 接口。
· 实现了 System.Collections.IEnumerable 接口。
· (从C# 9之后)可以绑定到名为 GetEnumerator 的扩展方法,并且该扩展方法返回枚举器(请参见4.9节)。
枚举类型模式如下:
以下是遍历单词beer中每一个字母的高级方法:
以下程序则是使用低层的调用,即不用 foreach 语句遍历单词beer的每一个字母的方法:
如果迭代器实现了 IDisposable ,则 foreach 语句也会起到 using 语句的作用,隐式销毁枚举器对象。
我们将在第7章详细介绍枚举相关的接口。
只需一个简单的步骤就可以实例化并填充可枚举对象:
编译器会将上述语句转换为:
它要求可枚举对象实现 System.Collections.IEnumerable 接口,并且有可以调用的带适当数量参数的 Add 方法。同样我们也能够使用类似的方式初始化字典(请参见7.5节):
或者更加简洁地写为:
第二种写法不仅适用于字典而且适用于任何具有索引器的类型。
foreach 语句是枚举器的消费者,而迭代器是枚举器的生产者。在本例中,我们使用迭代器来返回斐波那契数列(斐波那契数列表中的每一个数字是前两个数字之和):
return 语句表示“这是该方法的返回值”,而 yield return 语句则表示“这是当前枚举器产生的下一个元素”。在每条 yield 语句中,控制都返回给调用者,但是必须同时维护调用者的状态,以便调用者枚举下一个元素的时候,能够继续执行该方法。该状态的生命周期是与枚举器绑定的。在调用者枚举结束之后,该状态就可以被释放。
编译器将迭代方法转换为实现了 IEnumerable<T> 或 IEnumerator<T> 的私有类。迭代器块中的逻辑被“反转”并分别进入编译器生成的枚举器类的 MoveNext 方法和 Current 属性。当调用迭代器方法的时候,所做的仅仅是实例化编译器生成的类,而迭代器代码并没有真正执行。编写的迭代器代码只有当开始枚举结果序列时才开始执行,通常使用 foreach 语句。
迭代器可以是局部方法(请参见3.1.3.2节)。
迭代器是包含一个或者多个 yield 语句的方法、属性或者索引器。迭代器必须返回以下四个接口之一(否则编译器会产生相应错误):
迭代器具有不同的语义,这取决于迭代器返回的是可枚举接口还是枚举器接口,我们将在第7章说明。
我们可以一次使用多个 yield 语句,例如:
return 语句在迭代器块中是非法的。如果希望提前退出迭代器块,应该使用 yield break 语句。我们可以将 Foo 修改为如下示例:
yield return 语句不能出现在带有 catch 子句的 try 语句块中:
yield return 语句也不能出现在 catch 或者 finally 语句块中。出现这些限制的原因是编译器必须将迭代器转换为带有 MoveNext 、 Current 和 Dispose 成员的普通类,而转换异常处理语句块会大大增加代码的复杂性。
但是可以在只带有 finally 语句块的 try 语句块中使用yield语句:
当枚举器到达序列末尾或被销毁时就可以执行 finally 语句块了。如果枚举提前结束,则 foreach 语句会隐式销毁枚举器,这是消费枚举器的安全且正确的做法。当显式使用枚举器时,一个陷阱是提前结束枚举而不销毁枚举器,从而绕过 finally 语句块的执行。我们可以将枚举器的使用显式包裹在 using 语句中来避免上述错误。
迭代器的可组合性高。我们可以扩展前面的示例,只输出偶数斐波那契数列:
每个元素只有到最后关头,即执行 MoveNext() 操作时才会进行计算,图4-1显示了随时间变化的数据请求和数据输出。
图4-1:组合序列
迭代器模式的可组合性对LINQ而言非常重要,我们将在第8章进行讨论。