C#中的迭代器和yield

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

在.NET中通过使用接口IEnumeratorIEnumerable及其泛型等价物。来封装 迭代器模式 。所谓 迭代器模式 即是:允许你访问一个数据项序列,能依次遍历该序列中的每一项元素,而无需关心元素在序列内部的组织形式。

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>IEnumberableIEnumberator<T>IEnumberator中的任意一个。函数参数不能是refout
  • trycatch语句里面不能出现yield return
  • 不能在匿名方法中用迭代器代码块,也就是yield

参考网页

C#中的IEnumerator 和 yield

Unity协程(Coroutine)原理深入剖析

Unity协程(Coroutine)原理深入剖析再续