Hello,Coders。我们除了天天的码 if…else…之外,还会不断的码出foreach。我今天要说的是:传统遍历需实现的接口及我们还有一种更简洁优雅的方式实现多种迭代器。
传统遍历
传统的遍历即通过让集合类实现IEnumerable、IEnumerator或IEnumerable<T>、IEnumerator<T>接口来支持遍历。
| public interface IEnumerable IEnumeratorGetEnumerator(); public interface IEnumerator |
- 分析:
1) 从这两个接口的用词选择上,也可以看出其不同:
a) IEnumerable是一个声明式的接口,声明实现该接口的类是可枚举。
b) IEnumerator是一个实现式的接口,IEnumerator对象说明如何实现枚举器。
2) Foreach语句隐式调用集合的无参GetEnumerator方法(不论集合是否有实现IEnumerable接口,但只要有无参GetEnumerator方法并返回IEnumerator就可遍历)。
3) 集合类为什么不直接实现IEnumerable和IEnumerator接口?
这样是为了提高并发性。Eg:一个遍历机制只有一个Current,一旦并发就会出错。然而“将遍历机制与集合分离开来”如果要实现同时遍历同一个集合,只需由GetEnumerator() 创建一个新的包含遍历机制(IEnumerator)的类实例即可。
- 调用过程
插播一段:由foreach执行过程可知其迭代器是延迟计算的。
因为迭代的主体在MoveNext() 中实现,foreach中每次遍历执行到 in 的时候才会调用MOveNext() ,所以其迭代器耗时的指令是延迟计算的。
延迟计算(Lazy evaluation):来源自函数式编程,在函数式编程里,将函数作为参数来传递,传递过程中不会执行函数内部耗时的计算,直到需要这个计算结果的时候才调用,这样就可以因为避免一些不必要的计算而改进性能。 (另外还有linq、DataReader等也运用了延迟计算的思想)
- 具体实现示例
| public Course(String name) private String name = string.Empty; public class CourseCollection : IEnumerable<Course> public CourseCollection() arr_Course = new Course[] private Course[] arr_Course; public Course this [int index] get { return arr_Course[index]; } get { return arr_Course.Length; } public IEnumerator<Course> GetEnumerator() return new CourseEnumerator( this ); #region 实现 IEnumerable<T> private sealed class CourseEnumerator : IEnumerator<Course> private readonly CourseCollection courseCollection; internal CourseEnumerator(CourseCollection courseCollection) this .courseCollection = courseCollection; get { return courseCollection[index]; } bool IEnumerator.MoveNext() return (index < courseCollection.Count); |
有了对“传统遍历”实现方式的理解才能快速明白下一节“迭代器”的实现原理。要知道绝大部分最新的概念其实都可以用最简单的那些概念组合而成。而只有对基本概念理解,才能看清那些复杂概念的实质。
迭代器(iterator)
迭代器是 C# 2.0 中的新功能。它使类或结构支持foreach迭代,而不必“显示”实现IEnumerable或IEnumerator接口。只需要简单的使用 yield 关键字,由 JIT 编译器帮我们编译成实现 IEnumerable或IEnumerator 接口的对象(即:本质还是传统遍历,只是写法上非常简洁)。
- 分析
1) yield 语句只能出现在 iterator 块(迭代块)中,该块只能用作方法、运算符或get访问器的主体实现。这类方法、运算符或访问器的“主体”受以下约束的控制:
a) 不允许不安全块。
b) 方法、运算符或访问器的参数不能是 ref 或 out。
2) 迭代器代码使用 yield return 语句依次返回每个元素。yield break 将终止迭代。
a) yield return 的时候会保存当前位置并把控制权从yield所在程序交给调用的程序,下次调用迭代器时将从此位置重新开始执行,并继续下一个语句。
b) yield break就是交给调用程序就不回来了从而终止迭代。
3) yield return 语句不能放在 try-catch 块中。但可放在后跟 finally 块的 try 块中。
4) yield break 语句可放在 try 块或 catch 块中,但不能放在 finally 块中。
5) yield 语句不能出现在匿名方法中。
6) 迭代器必须返回相同类型的值,因为最后输出为IEnumerator.Current是单一类型。(见下面示例)
7) 在同一个迭代器中可以使用多个 yield 语句。(见下面示例)
8) 自定义迭代器:迭代器可以自定义名称、可以带参数,但在foreach中需要显示去调用自定义的迭代器。(见下面示例)
9) 迭代器的返回类型必须为IEnumerator、IEnumerator<T>或IEnumerable、IEnumerable<T>。(见下面示例)
- 迭代器的具体实现
1) 返回类型为IEnumerator、IEnumerator<T>
返回此类型的迭代器方法必须满足:
a) 必须有GetEnumerator且不带参数;
b) 必须是public公共成员;
见示例代码,我们将CourseCollection集合对象的IEnumerable.GetEnumerator() 方法实现如下:
| public IEnumerator<String>GetEnumerator() for (inti = 0; i<arr_Course.Length; i++) Course course = arr_Course[i]; yield return "选修:" +course.Name; yield return Environment.NewLine; |
经过 JIT 编译后,会自动生成一个实现了 IEnumerator<String> 接口的对象。具体代码可通过 Reflector 工具查看,下面展示其中的MoveNext() 代码:
| switch ( this .<>1__state) this .<>7__wrap3 = this .<>4__this.arr_Course; while ( this .<>7__wrap4 < this .<>7__wrap3.Length) this .<course>5__1 = this .<>7__wrap3[ this .<>7__wrap4]; this .<>2__current = "选修:" + this .<course>5__1.Name; this .<>2__current = Environment.NewLine; this .System.IDisposable.Dispose(); |
通过代码,我们可以知道:
a) 同一个迭代器中有多少个 yield return语句,while 循环中就有多少个 return true 。
b) 每一个 yield retuen语句会调用一次MoveNext()方法。输出数据的顺序通过生成类中的一个state字段做为 switch 标识来决定要输出第几个 yield return 。而yield break 语句是通过将 state 字段设置为 switch 中没有的值(eg:-1)从而使后续每次迭代进入MoveNext()后因为 switch 匹配不到而没有输出数据。
1) 返回类型为IEnumerable、IEnumerable<T>
返回此类型的迭代器必须满足:
a) 必须可以在foreach语句中被调用(访问权限);
返回此类型的迭代器通常用于实现自定义迭代器,即:迭代器可以自定义名称、可以带参数。Eg:(升序和降序)
| publicIEnumerable<String>GetEnumerable_ASC() publicIEnumerable<String>GetEnumerable_DESC() for (inti = arr_Course.Length - 1; i>= 0; i--) Course course = arr_Course[i]; yield return "选修:" +course.Name; yield return Environment.NewLine; |
需如下进行迭代器调用:
yield_Example.CourseCollection col2 = new yield_Example.CourseCollection();
foreach (String str in col2.GetEnumerable_ASC()){ // col2.GetEnumerable_ASC()}
foreach (String str in col2.GetEnumerable_DESC()){//col2.GetEnumerable_DESC()}
经过 JIT 编译后,会自动生成一个直接实现IEnumerator<String>和IEnumerator<String>接口的对象,其GetEnumerator() 方法返回自己(this)。
这是因为在不同foreach遍历中访问的每个返回的枚举器具有其自己的状态,并且一个枚举器和其他枚举器互不影响(即每个枚举器都是实现IEnumerable<T>接口的新对象),不存在并发的问题。