枚举器和迭代器
- 枚举器、可枚举类型
- 数组可以按需提供一个叫做枚举器的对象
- 枚举器可以依次返回请求的数组中的元素
- 获取一个对象枚举器的方法是调用
GetEnumerator
方法,实现该方法的类型称为可枚举类型 foreach
结构设计用来和可枚举类型一起使用,只要遍历对象是可枚举类型,就会执行:- 调用
GetEnumerator
方法获取对象的枚举器 - 从枚举器中请求每一项并且把它作为迭代变量,代码可以读取该变量但是不可改变
- 调用
IEnumerator
接口- 实现了
IEnumerator
接口的枚举器包含三个函数成员:current
,MoveNext
,Reset
Current
返回当前位置的属性- 只读
- 返回
object
类型的引用,所以可以返回任何类型
MoveNext
把枚举器前进到集合中的下一项,并返回新位置是否有效- 初始位置在第一项之前,所以使用
Current
前必须至少调用一次MoveNext
- 初始位置在第一项之前,所以使用
Reset
重置枚举器的位置- 利用枚举器模仿foreach循环遍历集合中的项
-
int[] MyArray = {1, 2, 3, 4, 5}; IEnumerator ie = MyArray.GetEnumerator(); while (ie.MoveNext()) { System.Console.WriteLine((int)ie.Current); }
- 编写foreach时,C#编译器会生成与上述代码逻辑十分类似CIL形式的代码
- 实现了
IEnumerable
接口IEnumerable
接口只有一个成员:GetEnumerator
方法,返回对象的枚举器
- 使用
IEnumerator
接口和IEnumerable
接口- 可枚举类型实现
IEnumerable
接口,实现方法GetEnumerator
方法,返回值为另一个枚举器对象 - 枚举器对象实现
IEnumerator
接口,实现方法MoveNext
、Reset
以及属性Current
- 可枚举类型实现
- 泛型枚举接口
- 非泛型接口形式
IEnumerable
接口的GetEnumerator
方法返回实现IEnumerator
枚举器类的实例- 实现
IEnumerator
的类实现了Current
属性,它返回object
引用,使用需要强转为实际类型 - 实现不是类型安全的,在手动强转时可能会发生异常
- 泛型接口
IEnumerable<T>
接口的GetEnumerator
方法返回IEnumerator<T>
的枚举器类的实例- 实现
IEnumerator<T>
的类实现的Current
类型返回实际类型的的对象 - 返回实际类型的引用,是类型安全的,如果要自己创建可枚举类,应该实现这些泛型接口
- 注意
- 泛型接口也实现了非泛型接口,目的貌似是强制向下兼容,因为在C#2.0版本以前没有泛型
- 因此需要提供泛型与非泛型两个版本的方法实现
- 由于泛型接口和非泛型接口的方法/属性同名同参数列表但是返回类型不同(
Current
和GetGetEnumerator
) - 所以需要使用显式接口成员实现,返回类型一致的如
MoveNext
、Reset
只需要提供一份实现即可 - 另外
IEnumerator<T>
泛型版本还实现了IDisposable
接口,所以还需要手动实现Dispose
方法释放资源
- 非泛型接口形式
- 迭代器
- C#从2.0版本开始提供了更简单的创建枚举器和可枚举类型的方法。实际上,编译器将为我们创建它们,这种结构叫做迭代器
- 迭代器块
- 有一个或多个yield语句的代码块
- 迭代器块与其他语句块不同,其他块包含的语句被当作是命令式的。也就是说,先执行代码的第一个语句,然后执行后面的语句,最后控制离开块
- 另一方面,迭代器块不是需要在同一时间执行的一串命令式命令,而是描述了希望编译器为我们创建的枚举器类的行为,迭代器块中的代码描述了如何枚举元素
- 两个特殊语句
- yield return语句指定了序列中返回的下一项
- yield break语句指定在序列中没用其他项
- 使用迭代器来创建枚举器和可枚举类型
-
class Test { int[] arr = {1, 2, 3, 4, 5}; public IEnumerator<int> GetEnumerator() { return TestEnumerator(); } IEnumerator<int> TestEnumerator() { for (int i = 0; i < arr.Length; i++) { yield return arr[i]; } } }
- 不需要实现
IEnumerator
接口,TestEnumerator
返回值即为枚举器
-
- 使用迭代器创建可枚举类型
-
class Test { int[] arr = {1, 2, 3, 4, 5}; public IEnumerator<int> GetEnumerator() { return TestEnumerator().GetEnumerator(); } IEnumerable<int> TestEnumerator() { for (int i = 0; i < arr.Length; i++) { yield return arr[i]; } } }
- 不需要实现
IEnumerable
接口,TestEnumerator
返回值即为可枚举类型,同时Test
类本身也是可枚举类型 - 在使用
foreach
遍历时可以使用类对象,也可以使用类枚举器方法TestEnumerator()
-
- 常见迭代器模式
- 实现返回枚举器的迭代器时,必须通过实现
GetEnumerator
来让类可枚举,它返回由迭代器返回的枚举器 - 如果实现迭代器返回可枚举类型
- 可以实现
GetEnumerator
让类可枚举,让它调用迭代器方法以获取自动生成的实现IEnumerable
的类实例,然后可枚举类型对象返回由GetEnumerator
创建的枚举器 - 也可以不实现
GetEnumerator
,此时类本身不可枚举,但迭代器返回的是可枚举类,可以直接调用迭代器方法
- 可以实现
- 实现返回枚举器的迭代器时,必须通过实现
- 产生多个可枚举类型
- 同一个类可以产生多个可枚举类型,只需要实现多个迭代器方法,这些方法返回可枚举类型,然后通过迭代器去获得可枚举类型,可以不实现
GetEnumerator
- 同理,一个类可以产生多个枚举器,但由于让类可枚举必须实现
GetEnumerator
方法,具体返回哪个迭代器的枚举器,可以通过一个布尔变量来进行选择
- 同一个类可以产生多个可枚举类型,只需要实现多个迭代器方法,这些方法返回可枚举类型,然后通过迭代器去获得可枚举类型,可以不实现
- 将迭代器作为属性
- 使用类似迭代器方法,只不过把迭代器块作为属性的get访问器来声明
- 注意事项
- 迭代器需要
System.Collections.Generic
命名空间 - 使用迭代器时会由编译器产生枚举器,但这个枚举器中没有实现Reset方法,调用时会抛出`System.NotSupportedException异常
- 迭代器需要
- 迭代器实质
- 由编译器生成的枚举器类是包含四个状态的状态机
- Before:首次调用
MoveNext
的初始状态 - Running:调用
MoveNext
后进入这个状态。在这个状态中,枚举器检测并设置下一项的位置。在遇到yield return
、yield break
或在迭代器体结束时,退出状态 - Suspended:状态机等待下次调用
MoveNext
的状态 - After:没有更多项可以枚举
- Before:首次调用
- 如果状态机在Before或Suspended状态时调用了MoveNext方法,就转到Running状态。
- Running状态结束时,如果有更多项,状态机转入Suspended状态,否则转入并保持在After状态
- 由编译器生成的枚举器类是包含四个状态的状态机