C#中的迭代器和yield

Table of Contents

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

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

1.非泛型版本的IEnumerator和IEnumerable接口

首先看IEnumerator接口的定义,代码如下:

public interface IEnumerator
{
    /*
    获取集合中位于枚举数当前位置的元素。在下列任一情况下,不定义Current:
    
    1 枚举器位于集合中第一个元素之前,紧跟在创建枚举器之后。 在读取Current的值之前,
      必须调用MoveNext,以将枚举器前进到集合的第一个元素。
    2 对MoveNext的最后一次调用返回了 false,指示集合的末尾。
    3 由于对集合所做的更改(如添加、修改或删除元素),枚举器无效。
    
    在调用 Current 之前,MoveNext返回相同的对象。 MoveNext将Current设置为下一个元素。
    */
    object Current { get; }

    /* 
    将枚举数推进到集合的下一个元素。
    如果枚举数已成功地推进到下一个元素,则为true;
    如果枚举数传递到集合的末尾,则为false。

    在创建枚举器之后,或在调用Reset方法之后,枚举数将定位到集合的第一个元素之前,
    对MoveNext方法的第一次调用将枚举器移动到集合的第一个元素上。

    如果MoveNext越过集合的末尾,则枚举器将定位到集合中的最后一个元素之后,MoveNext返回false。
    当枚举器位于此位置时,对MoveNext的后续调用也将返回false,直到调用Reset。

    如果对集合所做的更改(如添加、修改或删除元素),则MoveNext的行为是不确定的。
    */
    bool MoveNext();

    /*
    将枚举数设置为其初始位置,该位置位于集合中第一个元素之前。
    */
    void Reset();
}

顾名思义,IEnumerator就是表征一个“ 迭代器 ”(又可称为 枚举器 )应有的功能接口,该接口所函数的方法如上述代码注释中所描述,接下来看IEumerable接口,代码如下:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

IEnumerable用来表征“一个数据集合是 可迭代 的( 可枚举 的)”,即是说该数据集合中的每个元素都是可以被一一迭代的。IEnumerable接口也很简单,只有一个方法,就是返回 能访问到该数据集合每个元素的迭代器

通过上述两个interface可以看到,如果一个数据集合,要定义其为可枚举的,则它需要继承实现IEnumerable接口。并且同时得定义实现一个迭代器类,这个类要继承实现IEnumerator接口。下面举示例代码说明:

class IteratableDataStruct : IEnumerable
{
    object[] Values;

    // 数据集合中,作为第一个元素的位置索引值
    public int StartingPoint
    {
        get;
        set;
    }
 
    public IteratableDataStruct(object[] values, int startingPoint)
    {
        this.values = values;
        this.StartingPoint = startingPoint;
    }
 
    public IEnumerator GetEnumerator()
    {
        return new DataStructIterator(this);
    }

    // 迭代器实现
    class DataStructIterator : IEnumerator
    {
        // 重要数据成员,来获取当前值,下一个值,是否结束。在构造函数中赋值。
        IteratableDataStruct m_DataStruct;

        // 迭代器游标
        int m_IteratorPosition;
    
        internal public IterationSampleIterator(IteratableDataStruct ds)
        {
            m_DataStruct = ds;
            m_IteratorPosition = -1; // 迭代器游标按照标准,初始化时,游标器在表头第一项之前
        }
 
        // 重载实现interface IEnumerator的同名方法
        public bool MoveNext()
        {
            // 如果没有超出数组的长度,就允许迭代器游标位置增加
            if (m_IteratorPosition != m_DataStruct.values.Length)
            {
                m_IteratorPosition++;
            }

            // 按MoveNext方法的定义,迭代器游标超出
            return m_IteratorPosition < parent.values.Length;
        }
 
        // 重载实现interface IEnumerator的同名属性
        public object Current
        {
            get
            {
                // 如果当前迭代器游标位置不合法就排除异常
                if (m_IteratorPosition == -1 || m_IteratorPosition == parent.values.Length)
                {
                    throw new InvalidOperationException();
                }

                int index = (m_IteratorPosition + m_DataStruct.StartingPoint);
                index = index % m_DataStruct.values.Length;
                return m_DataStruct.values[index];
            }
        }
 
        // 重载实现interface IEnumerator的同名方法
        public void Reset()
        {
            // 迭代器游标按照标准,重置时,游标器在表头第一项之前
            m_IteratorPosition = -1;
        }
    } // class DataStructIterator
} // class IteratableDataStruct

上面代码段就是对迭代器和可迭代数据结构的定义。下面的伪代码段则演示了如何迭代遍历数据结构内的每一个元素:

var ds = new IteratableDataStruct(...); // 
var iterator = sample.GetEnumerator(); // 拿到该data struct对应的迭代器

while(iterator.MoveNext()) // 调用迭代器的MoveNext方法,依次获取每一个元素
{
  item = iterator.Current;
  .....
}

如果不想显式地使用数据结构的GetEnumerator方法,以及迭代器的MoveNext方法和Current属性, 可以使用语法关键字foreach,把上面的代码 合三为一 ,如下:

// 如果不想显式地使用数据结构的GetEnumerator方法,以及迭代器的MoveNext方法,
// 可以使用语法关键字foreach,在一个循环内遍历每一个元素
foreach(var item in sample) 
{
    .....
}

上面的定义和使用迭代器的方法,是C#1.0时代就有的。代码非常直观易懂,但就是略显繁琐。比如,要操作的数据其实都在继承实现了IEnumerable接口的IteratableDataStruct类中,为了操作在此类中的数据,不得不再定义实现另一个辅助工具性质的类。所以从C#2.0时代开始,引入了yield return关键字,使用此关键字,可以省略了作为工具性质的、显式地对IEnumertor接口进行继承实现的迭代器类,而只需要在IEnumerable接口中的GetEnumerator()方法中,定义如何去迭代集合中的每一个元素的方法就好。所以上述的代码使用yield return关键字可以改写为:

class IteratableDataStruct : IEnumerable
{
    object[] Values;

    // 数据集合中,作为第一个元素的位置索引值
    public int StartingPoint
    {
        get;
        set;
    }
 
    public IteratableDataStruct(object[] values, int startingPoint)
    {
        this.Values = values;
        this.StartingPoint = startingPoint;
    }
 
    public IEnumerator GetEnumerator()
    {
        for (int index = 0; index <Values.Length; index++)
        {
            yield return Values[(index + StartingPoint) % Values.Length];
        }
    }
}

如果刚开始看这个用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)原理深入剖析再续

kumakoko avatar
kumakoko
pure coder
comments powered by Disqus