阅读 40

yield为什么可以实现迭代器功能

实现foreach遍历的两种方式

使用IEnumerable 与 IEnumerator接口

  首先先来了解一下这两个接口的含义,从名字常来看,IEnumerator是枚举器的意思,IEnumerable是可枚举的意思。接着看源码:

IEnumerable

public interface IEnumerable
{
    // Interfaces are not serializable
    // Returns an IEnumerator for this enumerable Object.  The enumerator provides
    // a simple way to access all the contents of a collection.
    [Pure]
    [DispId(-4)]
    IEnumerator GetEnumerator();
}
复制代码

IEnumerator

public interface IEnumerator
{
    // Interfaces are not serializable
    // Advances the enumerator to the next element of the enumeration and
    // returns a boolean indicating whether an element is available. Upon
    // creation, an enumerator is conceptually positioned before the first
    // element of the enumeration, and the first call to MoveNext 
    // brings the first element of the enumeration into view.
    // 
    bool MoveNext();

    // Returns the current element of the enumeration. The returned value is
    // undefined before the first call to MoveNext and following a
    // call to MoveNext that returned false. Multiple calls to
    // GetCurrent with no intervening calls to MoveNext 
    // will return the same object.
    // 
    Object Current {
        get; 
    }

    // Resets the enumerator to the beginning of the enumeration, starting over.
    // The preferred behavior for Reset is to return the exact same enumeration.
    // This means if you modify the underlying collection then call Reset, your
    // IEnumerator will be invalid, just as it would have been if you had called
    // MoveNext or Current.
    //
    void Reset();
}
复制代码

  IEnumerable中只有一个GetEnumerator函数,返回值是IEnumerator类型,所以实现了IEnumerable接口的类可以通过此方法获取一个IEnumerator枚举器,并通过此枚举器遍历这个类中包含的集合中的元素的功能(比如List,ArrayList,Dictionary等继承了IEnumeratble接口的类)。

实现代码

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SuanFa.IEnumerableInterface
{
    class InheritIEnumerable : IEnumerable
    {
        public int[] array = new int[] { 1, 3, 4 };  
        static void Main()
        {
            InheritIEnumerable ii = new InheritIEnumerable();
            Console.WriteLine("foreach执行结果:");
            foreach (int i in ii)
            {
                Console.WriteLine("i=" + i);
            }
        }
        /// <summary>
        /// 返回IEnumerator类型的对象
        /// </summary>
        /// <returns></returns>
        public IEnumerator GetEnumerator()
        {
            Console.WriteLine("获取枚举器");
            return new InheritIEnumerator(array);
        }
    }
    public class InheritIEnumerator : IEnumerator
    {
        int[] array;
        int pos = -1;
        public InheritIEnumerator(int []array)
        {
            this.array = array;
        }
        public object Current
        {
            get
            {
                Console.WriteLine("获取Current");
                if (pos<array.Length)
                {
                    return array[pos];
                }
                else
                {
                    throw new InvalidOperationException();
                }
            }
        }
        public bool MoveNext()
        {
            
            if (pos<array.Length-1)
            {
                Console.WriteLine("MoveNext true");
                pos++;
                return true;
            }
            else
            {
                Console.WriteLine("MoveNext false");
                return false;
            }
        }
        public void Reset()
        {
            pos = -1;
        }
    }
}

复制代码

运行结果

  

分析

通过输出结果,可以发现,foreach在运行时会先调用InheritIEnumerable的GetIEnumerator函数获取一个InheritIEnumerator实例(枚举器实例),之后通过循环调用InheritIEnumerator的MoveNext函数,pos后移,更新Current属性,然后返回Current属性,直到MoveNext返回false。 总结一下:

  • GetIEnumerator()负责获取枚举器。
  • MoveNext()负责让Current获取下一个值,并判断遍历是否结束。
  • Current负责返回当前指向的值。
  • Rest()负责重置枚举器的状态(在foreach中没有用到)

这些就是IEnumerable,IEnumerator的基本工作原理。   所以foreach就相当于一下代码:

IEnumerable ieable = new InheritIEnumerable();
            IEnumerator ie = ieable.GetEnumerator();
while (ie.MoveNext())
{
    Console.WriteLine(ie.Current);
}
复制代码

使用yield

代码实现

 class YieldFunctions
{
    public static IEnumerable<int> getNums()
    {
        yield return 1;
        yield return 0;
        yield return 3;
        yield break;
        yield return 5;
    }
    public static void Main()
    {
        foreach (int i in getNums())
        {
            Console.WriteLine(i);
        }
    }
}
复制代码
  • yield return :返回迭代器的内容
  • yield break :终止迭代
  • yield只能使用在返回类型必须为 IEnumerable、IEnumerable"T"、IEnumerator 或 IEnumerator"T"的方法、运算符、get访问器中

运行结果

关键问题--使用yield迭代的时候我们虽然没有实现GetEnumerator()方法,也没有实现对应的IEnumerator的MoveNext(),和Current属性,但是我们仍然能正常使用foreach,换句话说也就是yield关键字在编译过程中会发生了什么?

YieldFunctions的IL代码

先看一下YieldFunctions类的IL代码的整体框架,主要有三部分:getNums生成的IL代码,新生成的getNums类的IL代码,Main方法的IL代码。

接下来查看一下getNums方法生成的IL代码,可以返现在该方法中,创建了一个getNums类的对象,并将其作为返回值。
然后看一下生成的新的类getNums的代码:

再然后可以看一下MoveNext代码:

总结

用yield来进行迭代的真实流程就是:

  • 运行getNums()函数,获取代码自动生成的类的实例(IL代码中getNums类的实例)。
  • 接着调用IL代码中GetEnumberator()函数,将获取的类自己作为迭代器开始迭代。
  • 每次运行MoveNext(),state增加1,通过switch语句可以让每次调用MoveNext()的时候执行不同部分的代码。
  • MoveNext()返回false,结束。

个人总结

  • yield关键字其实是一种语法糖,最终还是通过实现IEnumberable"T"、IEnumberable、IEnumberator"T"和IEnumberator接口实现的迭代功能。
  • 综上两种对foreach的实现原理,不管哪种方式,其最终原理还是一样的,只是体现了C#的封装性更好,对程序员更加友好,但为了更好的编程,还是需要了解这些原理。
  • 可以说,如何可以使用foreach进行遍历的类,都需要直接或间接地实现IEnumerable接口,并返回IEnumerator枚举器对象进行遍历。
关注下面的标签,发现更多相似文章
评论