迭代器模式是行为模式的一种范例,行为模式是一种简化对象之间通信的设计模式。这是一种非常易于理解和使用的模式。实际上,它允许你访问一个数据项序列中的所有元素,而无须关心序列是什么类型——数组、列表、链表或任何其他类型。它能非常有效地构建出一个数据管道,经过一系列不同的转换或过滤后再从管道的另一端出来。
在.NET中,迭代器模式是通过IEnumerator和IEnumerable接口及它们的泛型等价物来封装的。如果某个类型实现了IEnumerable接口,就意味着它可以被迭代访问。调用GetEnumerator方法将返回IEnumerator的实现,这就是迭代器本身。C# 1利用foreach语句实现了访问迭代器的内置支持。foreach语句被编译后会调用GetEnumerator和MoveNext方法以及Current属性,假如IDisposable也实现了,程序最后还会自动销毁迭代器对象。
一、C# 1:手写迭代器的痛苦
实现一个相对简单的迭代器首先需要实现IEnumerable接口,以便使用者能够轻松迭代集合中的所有值。其次需要实现GetEnumerator方法,以获取当前迭代的对象信息。如果将GetEnumerator的实现放在与实现IEnumerable接口相同的类中,会存在一定的问题,比如使用两个嵌套的foreach语句,那么就要求两个迭代器彼此独立,而放在一个类中会导致分工不明确,代码也会变得非常混乱,所有通常情况下会将GetEnumertor放在一个单独实现的类中。
实现如下:
二、C# 2:利用yield语句简化迭代器
1、迭代器块和yield return实现
1、C# 2中GetEnumerator方法的完整实现如下:
yield return就是告诉C#编译器,这个方法不是一个普通的方法,而是实现一个迭代器块的方法。这个方法被声明为返回一个IEnumerator接口,所以就只能使用迭代器块来实现返回类型为IEnumerable、IEnumerator或泛型等价物的方法。如果方法声明的返回类型是非泛型接口,那么迭代器块的生成类型(yield type)是object,否则就是泛型接口的类型参数。
yield return的限制:如果存在任何catch代码块,则不能在try代码块中使用yield return,并且在finally代码块中也不能使用yield return或yield break。这并非意味着不能在迭代器内部使用try/catch或try/finally代码块,只是说使用它们时有一些限制而已。
2、编写迭代器块时,尽管你编写了一个似乎是顺序执行的方法,但实际上是请求编译器为你创建了一个状态机。这样做的原因,和我们在C# 1的迭代器实现中塞入那么多代码的原因完全一样——调用者每次只想获取一个元素,所以在返回上一个值时需要跟踪当前的工作状态。
当编译器看到迭代器块时,会为状态机创建一个嵌套类型,来正确记录块中的位置以及局部变量(包括参数)的值。所创建的类类似于我们之前用普通方法实现的类,用实例变量来保存所有必要的状态。要实现迭代器,状态机需要以下事情:
- 它必须具有某个初始状态;
- 每次调用MoveNext时,在提供下一个值之前(换句话说,就是执行到yield return语句之前),它需要执行GetEnumerator方法中的代码;
- 使用Current属性时,它必须返回我们生成的上一个值;
- 它必须知道何时完成生成值的操作,以便MoveNext返回false。
2、观察迭代器的工作流程
这个结果中有几个重要的事情需要牢记:
- 在第一次调用MoveNext之前,CreateEnumerable中的代码不会被调用;
- 所有工作在调用MoveNext时就完成了,获取Current的值不会执行任何代码;
- 在yield return的位置,代码就停止执行,在下一次调用MoveNext时又继续执行;
- 在一个方法中的不同地方可以编写多个yield return语句;
- 代码不会在最后的yield return处结束,而是通过返回false的MoveNext调用来结束方法的执行。
第一点尤为重要,因为它意味着如果在方法调用时需要立即执行代码,就不能使用迭代器块,如参数验证。如果你将普通检查放入用迭代器块实现的方法中,将不能很好地工作。
3、进一步了解迭代器执行流程
在常规的方法中,return语句具有两个作用:第一,给调用者提供返回值;第二,终止方法的执行,在退出时执行合适的finally代码块。我们看到yield return语句临时退出了方法,直到再次调用MoveNext后又继续执行,我们根本没有检查finally代码块的行为。
1、使用yield break结束迭代器的执行
yield break语句会终止迭代器的运行,让当前对MoveNext的调用返回false。yield break语句的行为非常类似于普通方法中的return语句。现在的问题是finally代码块如何执行及何时执行?
2、finally代码块的执行
在要离开相关作用域时,我们习惯执行finally代码块。迭代器块行为方式和普通方法不太一样,尽管我们也看到,yield return语句暂时停止了方法,但并没有退出该方法。按照这样的逻辑,在这里我们不要期望任何finally代码块能够正确执行——实际也确实如此。不过,在遇到yield break语句时,适当的finally代码块还是能够执行的。
finally在迭代器块中常用于释放资源,通常与using语句配合使用。foreach会在它自己的finally代码块中调用IEnumerator所提供的Dispose方法(就像using语句)。当迭代器完成迭代之前,你如果调用由迭代器代码块创建的迭代器上的Dispose,那么状态机就会执行在代码当前“暂停”位置范围内的任何finally代码。简单来说,只要调用者使用了foreach循环,迭代器块中的finally将按照你期望的方式工作。
在迭代器完成之前终止它的执行是相当少见的,并且不用foreach语句而手动使用迭代器也是不多见的,不过如果你这样做,记得把迭代器包含在using语句中使用
4、体实现中的奇特之处
微软C#2编译器对迭代器的实现有以下奇特之处值得了解:
- 在第一次调用MoveNext之前,Current属性总是返回迭代器产生类型的默认值;
- 在MoveNext返回false之后,Current属性总是返回最后的生成值;
- Reset总是抛出异常,而不像我们手动实现的重置过程那样,为了遵循语言规范,这是必要的行为;
- 嵌套类总是实现IEnumerator的泛型形式和非泛型形式(提供给泛型和非泛型的IEnumerable所用)。
不实现Reset是完全合理的——编译器无法合理地解决在重置迭代器时需要完成的一些事情,甚至不能判断解决方法是否可行。可以认为,Reset在IEnumerator接口中一开始就不存在,很多集合都不支持Reset,通常,调用者不能依赖它。