19.1 枚举器和可枚举类型
使用 foreach 语句
? 下面是使用 foreach 语句遍历数组的示例。
? 数组可以使用这种方式访问的原因是:数组提供了枚举器对象。枚举器知道数组中元素的次序,并依次返回数组中的元素。
? 对于有枚举器的类型而言,我们使用 GetEnumerator 方法来获取其拥有的枚举器,实现该方法的类型称为可枚举类型。数组就是可枚举类型。
图19.1 枚举器和可枚举类型概览
? foreach 结构设计用来和可枚举类型一起使用,如下行为会被执行:
- 调用 GetEnumerator 方法获取对象的枚举器。
- 从枚举起中请求每一项并作为迭代变量(可读不可写)。
19.2 IEnumerator 接口
? 实现了 IEnumerator 接口的枚举器包含 3 个函数成员:
Current
:返回序列中当前位置项的属性。
MoveNext
:是将枚举器位置前进到集合中下一项的方法,返回布尔值。
- 如果新的位置有效,返回 true。
- 如果新的位置无效,返回 false。
Reset
:将位置重置为原始状态。
- 枚举器的原始位置在序列中的第一项之前,因此 MoveNext 必须在第一次使用 Current 之前调用。
图19.2 小集合的枚举器
? 在编写 foreach 循环的时候,C# 编译器将生成与下面十分类似的代码(以 CIL 的形式)。
图19.3 .NET 数组类实现了 IEnumerator
19.3 IEnumerable 接口
? 可枚举类是指实现了 IEnumerable 接口的类,该接口只有一个成员——GetEnumerator,返回对象的枚举器。
图19.4 GetEnumerator 方法返回类的一个枚举器对象
使用 IEnumerable 和 IEnumerator 的示例
19.4 泛型枚举接口
? 使用 C# 泛型和非泛型的方式相差不大。
- 对于非泛型接口形式:
- IEnumerable 接口的 GetEnumerator 方法返回实现 IEnumerator 的枚举器类实例。
- 实现 IEnumerator 的类实现了 Current 属性,返回 object 类型的引用,然后将其转化为对象的实际类型。
- 对于泛型接口形式:
- IEnumerable<T> 接口的 GetEnumerator 方法返回实现 IEnumerator<T> 的枚举器类实例。
- 实现 IEnumerator<T> 的类实现了 Current 属性,返回实际类型的实例,而不是 object 基类的引用。
- 实际上泛型接口的声明是协变的,即 IEnumerable<out T> 和 IEnumerator<out T>,因此这些接口的对象可以是派生的类型。
? 泛型版本简单易用,但其结构略显复杂。
图19.5 实现 IEnumerable<T> 接口的类的结构
19.5 迭代器
? 可枚举类和枚举器在 .NET 集合类中被广泛使用,从 C# 2.0 版本开始提供了创建枚举器和可枚举类型更简单的方式,将这种结构称为迭代器。
? 在下面这个示例中:
- 迭代器返回一个泛型枚举器,该枚举器返回 3 个 string 类型的项。
- yield return 语句声明这是枚举中的下一项。
? 下面的方法声明了另一个版本,输出结果与上面相同。
? 枚举器不会一次返回所有的元素,而是每次访问 Current 属性时返回一个新值。
19.5.1 迭代器块
? 迭代器块是有一个或多个 yield 语句的代码块,可以是如下任意一种代码块:
? 迭代器块描述了希望编译器为我们创建的枚举器类的行为,其中的代码描述了如何枚举元素。
yield return
:指定序列中要返回的下一项。yield break
:指定在序列中没有其他项。
? 编译器得到有关枚举项的描述后,会构建包含所有需要的方法(MoveNext)和属性(Current)实现的枚举器类,产生的类被嵌套包含在声明迭代器的类中。
图19.6 根据指定的返回类型,可以让迭代器产生枚举器或可枚举类型
19.5.2 使用迭代器来创建枚举器
- 图中左边演示了返回类型是 IEnumerator<string>。
- 图中右边演示了它有一个嵌套类实现了 IEnumerator<string>。
图19.7 迭代器块产生了枚举器
19.5.3 使用迭代器来创建可枚举类型
? 本节例子使用迭代器来创建可枚举类型,而不是枚举器。
- BlackAndWhite 迭代器方法返回 IEnumerable<string> 而不是 IEnumerator<string>。
- MyClass 首先调用 BlackAndWhite 方法获取可枚举类型对象,然后调用该对象的 GetEnumerator 方法来获取结果,从而实现 GetEnumerator 方法。
- 图中左边演示了返回类型是 IEnumerable<string>。
- 图中右边演示了它有一个嵌套类实现了 IEnumerator<string> 和 IEnumerable<string>。
图19.8 编译器生成的类是可枚举类型并返回一个枚举器。编译器还生成了方法 BlackAndWhite,返回可枚举对象
19.6 常见迭代器模式
-
实现返回枚举器的迭代器。
通过实现 GetEnumerator 方法让类可枚举,它返回由迭代器返回的枚举器。
-
实现返回可枚举类型的迭代器。
实现 GetEnumerator 来让类本身可枚举;或不实现,来让类不可枚举。
图19.9 常见的迭代器模式
19.7 产生多个可枚举类型
? Spectrum 类有两个可枚举类型的迭代器,但类本身不可枚举,因为没有实现 GetEnumerator 方法。
19.8 将迭代器作为属性
? 本示例演示两个方面的内容:
- 使用迭代器产生两个枚举器的类。
- 演示迭代器如何实现为属性,而不是方法。
? 这段代码声明了两个属性来定义两个不同的枚举器。GetEnumerator 方法根据 _listFromUVtoIR 布尔变量的值返回两个枚举器中的一个。如果 _listFromUVtoIR 为 true,则返回 UVtoIR 枚举器;否则,返回 IRtoUV 枚举器。
19.9 迭代器的实质
? 有关迭代器的其他重要事项:
- 迭代器需要 System.Collections.Generic 命名空间,因此需要使用 using 指令进行引入。
- 在编译器生成的枚举器中,不支持 Reset 方法。Reset 是接口需要的方法,所以实现了它,但调用时总是抛出 System.NotSupportedException 异常。
? 在后台,编译器生成的枚举器类总是包含 4 个状态的状态机。
- Before:首次调用 MoveNext 的初始状态。
- Running:调用 MoveNext 后进入这个状态。
- 枚举器检测并设置下一项的位置。
- 遇到 yield return、 yield break 或在迭代器中结束时,退出 Running 状态。
- Suspended:状态机等待下次调用 MoveNext 的状态。
- After:没有更多项可以枚举的状态。
图19.10 迭代器状态机