C#的yield语法糖研究

来源:互联网 发布:tomcat和apache整合 编辑:程序博客网 时间:2024/05/17 08:57
一直觉得C#的yield语法很神奇,最近抽空好好研究了一下,写篇文章总结总结
首先看如下一个代码段;
int[] arr = { 0, 1, 2, 3, 4};foreach (int i in arr){    Console.Write("{0} ", i);}


非常简单的一个数组,使用foreach语句访问数组中每一个元素然后输出。不过这里有个问题,相比于普通的循环用法如while和for,foreach是如何实现遍历数组中每个元素的呢?
在使用反编译工具ILSpy将生成的exe文件反编译后得到如下代码:
int[] arr = new int[]{    0,    1,    2,    3,    4};int[] array = arr;for (int j = 0; j < array.Length; j++){    int i = array[j];    Console.Write("{0} ", i);}


可以看到对于数组而言,foreach被简单地转换为for语句,那如果是一个list对象呢?
int[] arr = { 0, 1, 2, 3, 4};List<int> li = new List<int>(arr);foreach (int i in li){    Console.Write("{0} ", i);}


反编译之后的代码如下:
int[] arr = new int[]{    0,    1,    2,    3,    4};List<int> li = new List<int>(arr);using (List<int>.Enumerator enumerator = li.GetEnumerator()){    while (enumerator.MoveNext())    {        int i = enumerator.Current;            Console.Write("{0} ", i);    }    goto IL_5D;}



可以看到在foreach语句被转换为while语句,在循环之前使用GetEnumerator()函数返回了一个List<int>.Enumerator,这个类型实现了IEnumerator接口,这个接口中MoveNext函数用于移动到集合中的下一个元素,如果可以继续迭代则返回true,否则返回false,而Current属性则返回迭代器的当前值,这样就可以通过一个循环对一个集合中的元素进行遍历。
对于遍历,C#中有两个非常重要的接口:IEnumerable以及IEnumerator,前者表示一个可迭代的类型,这个接口中只有一个函数GetEnumerator()用于返回一个IEnumerator枚举器,因此对于实现了这一接口的类型如List等,foreach语句就可以获取其对应的迭代器然后进行迭代遍历(数组也是一种IEnumerable类型,即以IEnumerable为参数的地方可以传入一个数组,只不过对数组的迭代会进行特殊处理,直接用for循环访问元素);IEnumerator为一个枚举器接口,有两个函数MoveNext和Reset,以及一个属性Current指向IEnumerable的当前元素。
所以现在就很明了了,foreach语句用于遍历一个IEnumerable集合中的元素,其具体实现为先GetEnumerator()获取IEnumerator枚举器,然后每次while循环中使用Current获取当前元素,然后MoveNext转至下一个元素,从而实现遍历。

那么问题来了,加入我们想要自己实现一个特质的集合类型可以用foreach进行遍历那怎么办呢?首先这个类型需要实现IEnumerator接口或者其中有一个GetEnumerator()函数能够返回一个枚举器,另外我们还需要自己实现一个用于对这个集合进行迭代的枚举器类型,并实现其中的Current、MoveNext等函数,使其能够正确的对集合进行操作从而实现遍历。这个过程其实还是比较麻烦的,需要自己写很多代码,而且每次创建一个新的集合类型都需要做一次这样的事情,此外加入迭代的元素自身我们也想进行定制,比如每次迭代的数根据上一次进行计算,那就需要在MoveNext函数中维护当前的状态,然后下一次MoveNext恢复状态继续执行。那为什么不构建一个通用的语法可以很方便的返回集合中的元素用于foreach呢?yield语句就是这个作用。
这是一个典型的例子(摘自MSDN):
static void Main()        {            // Display powers of 2 up to the exponent of 8:            //int[] arr = { 0, 1, 2, 3, 4};            //List<int> li = new List<int>(arr);            foreach (int i in Power(2, 8))            {                Console.Write("{0} ", i);            }            while (true) {            }        }        public static System.Collections.Generic.IEnumerable<int> Power(int number, int exponent)        {            int result = 1;            for (int i = 0; i < exponent; i++)            {                result = result * number;                yield return result;            }        }


代码中不需要我们构建一个IEnumerable以及IEnumerator,yield语句为我们做了这些。这个例子中是对一个函数进行迭代,函数接收一个number和一个exponent参数,每次迭代返回当前乘方的结果,实际上编译器会将创建一个实现IEnumerable接口的类,并在这个类中实现了一个内部类实现IEnumerator接口,这个内部类中维护了一个状态机,每次MoveNext就会从上次yield return的地方重新开始执行,直到下一个yield return出现的位置,然后返回yield return后面的值。因此Power函数被改写,返回一个实现IEnumerable接口的类的实例,这样foreach语句在转换为while语句后就可以正确的进行GetEnumerator以及MoveNext和Current操作了。
这是对于一个函数或者说一个命名的转换操作。而对于一个类而言,其默认迭代在GetEnumerator()函数中:
static void Main()        {            A a = new A();            foreach (int i in a)            {                Console.Write("{0} ", i);            }        }        public class A        {            public IEnumerator<int> GetEnumerator()            {                yield return 100;                yield return 200;            }        }

像这样的写法就会输出“100 200”。这种情况下省去了创建IEnumerable的过程,而是直接创建一个实现IEnumerator接口的内部类,并实现其MoveNext等函数,这样foreach也就可以正确的执行了。
因此总结一下就是yield语句简化了程序员定制可迭代集合的过程,不需要自己去实现一些类,可以直接使用yield return语句表明每次迭代需要返回什么。这样的话不仅在类中可以方便的进行迭代,还可以在函数中方便的实现迭代,定制每次迭代需要返回的元素,极大的简化了的复杂性。

在Unity中,由于需要每一帧更新内容,有时候希望在一个函数的逻辑可以在多个帧中被执行(比如复杂的计算,可能单帧执行时间过长,或者是一个操作需要延时完成),并且每次执行完后函数的当前状态可以保存下来,下次直接在上次保存的地方重新开始执行,这样就可以极大的简化语法,不需要自己在Update函数中编写一些复杂的逻辑用于状态记录。
这个需求和yield return语句的用途很像,每个MoveNext函数被调用时会从上一个yield return的地方开始执行,直到下一个yield return出现,那么如果实现一套逻辑每一帧调用一次MoveNext不就可以实现单个函数逻辑在多帧内完成了么,Unity中就使用StartCoroutine()函数创建协程进行实现。
http://stackoverflow.com/questions/12932306/how-does-startcoroutine-yield-return-pattern-really-work-in-unity

0 0
原创粉丝点击