C#中的迭代器和yield
请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
在.NET中通过使用接口IEnumerator
和IEnumerable
及其泛型等价物。来封装 迭代器模式 。所谓 迭代器模式 即是:允许你访问一个数据项序列,能依次遍历该序列中的每一项元素,而无需关心元素在序列内部的组织形式。
1.非泛型版本的IEnumerator和IEnumerable接口
首先看IEnumerator接口的定义,代码如下:
1public interface IEnumerator
2{
3 /*
4 获取集合中位于枚举数当前位置的元素。在下列任一情况下,不定义Current:
5
6 1 枚举器位于集合中第一个元素之前,紧跟在创建枚举器之后。 在读取Current的值之前,
7 必须调用MoveNext,以将枚举器前进到集合的第一个元素。
8 2 对MoveNext的最后一次调用返回了 false,指示集合的末尾。
9 3 由于对集合所做的更改(如添加、修改或删除元素),枚举器无效。
10
11 在调用 Current 之前,MoveNext返回相同的对象。 MoveNext将Current设置为下一个元素。
12 */
13 object Current { get; }
14
15 /*
16 将枚举数推进到集合的下一个元素。
17 如果枚举数已成功地推进到下一个元素,则为true;
18 如果枚举数传递到集合的末尾,则为false。
19
20 在创建枚举器之后,或在调用Reset方法之后,枚举数将定位到集合的第一个元素之前,
21 对MoveNext方法的第一次调用将枚举器移动到集合的第一个元素上。
22
23 如果MoveNext越过集合的末尾,则枚举器将定位到集合中的最后一个元素之后,MoveNext返回false。
24 当枚举器位于此位置时,对MoveNext的后续调用也将返回false,直到调用Reset。
25
26 如果对集合所做的更改(如添加、修改或删除元素),则MoveNext的行为是不确定的。
27 */
28 bool MoveNext();
29
30 /*
31 将枚举数设置为其初始位置,该位置位于集合中第一个元素之前。
32 */
33 void Reset();
34}
顾名思义,IEnumerator
就是表征一个“ 迭代器 ”(又可称为 枚举器 )应有的功能接口,该接口所函数的方法如上述代码注释中所描述,接下来看IEumerable
接口,代码如下:
1public interface IEnumerable
2{
3 IEnumerator GetEnumerator();
4}
IEnumerable
用来表征“一个数据集合是 可迭代 的( 可枚举 的)”,即是说该数据集合中的每个元素都是可以被一一迭代的。IEnumerable
接口也很简单,只有一个方法,就是返回 能访问到该数据集合每个元素的迭代器 。
通过上述两个interface可以看到,如果一个数据集合,要定义其为可枚举的,则它需要继承实现IEnumerable
接口。并且同时得定义实现一个迭代器类,这个类要继承实现IEnumerator
接口。下面举示例代码说明:
1class IteratableDataStruct : IEnumerable
2{
3 object[] Values;
4
5 // 数据集合中,作为第一个元素的位置索引值
6 public int StartingPoint
7 {
8 get;
9 set;
10 }
11
12 public IteratableDataStruct(object[] values, int startingPoint)
13 {
14 this.values = values;
15 this.StartingPoint = startingPoint;
16 }
17
18 public IEnumerator GetEnumerator()
19 {
20 return new DataStructIterator(this);
21 }
22
23 // 迭代器实现
24 class DataStructIterator : IEnumerator
25 {
26 // 重要数据成员,来获取当前值,下一个值,是否结束。在构造函数中赋值。
27 IteratableDataStruct m_DataStruct;
28
29 // 迭代器游标
30 int m_IteratorPosition;
31
32 internal public IterationSampleIterator(IteratableDataStruct ds)
33 {
34 m_DataStruct = ds;
35 m_IteratorPosition = -1; // 迭代器游标按照标准,初始化时,游标器在表头第一项之前
36 }
37
38 // 重载实现interface IEnumerator的同名方法
39 public bool MoveNext()
40 {
41 // 如果没有超出数组的长度,就允许迭代器游标位置增加
42 if (m_IteratorPosition != m_DataStruct.values.Length)
43 {
44 m_IteratorPosition++;
45 }
46
47 // 按MoveNext方法的定义,迭代器游标超出
48 return m_IteratorPosition < parent.values.Length;
49 }
50
51 // 重载实现interface IEnumerator的同名属性
52 public object Current
53 {
54 get
55 {
56 // 如果当前迭代器游标位置不合法就排除异常
57 if (m_IteratorPosition == -1 || m_IteratorPosition == parent.values.Length)
58 {
59 throw new InvalidOperationException();
60 }
61
62 int index = (m_IteratorPosition + m_DataStruct.StartingPoint);
63 index = index % m_DataStruct.values.Length;
64 return m_DataStruct.values[index];
65 }
66 }
67
68 // 重载实现interface IEnumerator的同名方法
69 public void Reset()
70 {
71 // 迭代器游标按照标准,重置时,游标器在表头第一项之前
72 m_IteratorPosition = -1;
73 }
74 } // class DataStructIterator
75} // class IteratableDataStruct
上面代码段就是对迭代器和可迭代数据结构的定义。下面的伪代码段则演示了如何迭代遍历数据结构内的每一个元素:
1var ds = new IteratableDataStruct(...); //
2var iterator = sample.GetEnumerator(); // 拿到该data struct对应的迭代器
3
4while(iterator.MoveNext()) // 调用迭代器的MoveNext方法,依次获取每一个元素
5{
6 item = iterator.Current;
7 .....
8}
如果不想显式地使用数据结构的GetEnumerator
方法,以及迭代器的MoveNext
方法和Current
属性, 可以使用语法关键字foreach
,把上面的代码 合三为一 ,如下:
1// 如果不想显式地使用数据结构的GetEnumerator方法,以及迭代器的MoveNext方法,
2// 可以使用语法关键字foreach,在一个循环内遍历每一个元素
3foreach(var item in sample)
4{
5 .....
6}
上面的定义和使用迭代器的方法,是C#1.0时代就有的。代码非常直观易懂,但就是略显繁琐。比如,要操作的数据其实都在继承实现了IEnumerable
接口的IteratableDataStruct
类中,为了操作在此类中的数据,不得不再定义实现另一个辅助工具性质的类。所以从C#2.0时代开始,引入了yield return
关键字,使用此关键字,可以省略了作为工具性质的、显式地对IEnumertor
接口进行继承实现的迭代器类,而只需要在IEnumerable
接口中的GetEnumerator()
方法中,定义如何去迭代集合中的每一个元素的方法就好。所以上述的代码使用yield return
关键字可以改写为:
1class IteratableDataStruct : IEnumerable
2{
3 object[] Values;
4
5 // 数据集合中,作为第一个元素的位置索引值
6 public int StartingPoint
7 {
8 get;
9 set;
10 }
11
12 public IteratableDataStruct(object[] values, int startingPoint)
13 {
14 this.Values = values;
15 this.StartingPoint = startingPoint;
16 }
17
18 public IEnumerator GetEnumerator()
19 {
20 for (int index = 0; index <Values.Length; index++)
21 {
22 yield return Values[(index + StartingPoint) % Values.Length];
23 }
24 }
25}
如果刚开始看这个用yield return
编写的方法时,可能会觉得 有点儿别扭———明明要返回的是一个实现了IEumerator
接口的类的实例对象。怎么现在看起来是“返回”了数据集合中每一个要枚举的元素。这里的for循环,看起来倒是有点儿像在一个循环里面调用MoveNext
方法去访问每一个元素……
实质上,yield return关键字就类似于一个语法糖,相当于是把DataStructIterator
类的MoveNext
方法,Reset
方法和Current
属性,在IteratableDataStruct.GetEnumerator()
方法里头进行声明和定义。而实质上,编译器在碰到使用了yield return
语句中的代码后,确实会“帮忙生成”类似于前面的DataStructIterator
类的工具类。具体实现的原理可以参阅这篇文章:c# yield关键字的用法。
关于yield
- 可以在方法,属性和索引器中使用
yield
来实现迭代器。 使用yield
,函数返回类型必须是IEnumberable<T>
、IEnumberable
、IEnumberator<T>
和IEnumberator
中的任意一个。函数参数不能是ref
或out
。 try
和catch
语句里面不能出现yield return
。- 不能在匿名方法中用迭代器代码块,也就是
yield
。