龙空技术网

C#核心-迭代器揭秘2

爱国程序员 400

前言:

现在姐妹们对“c 什么是迭代器”都比较重视,咱们都需要了解一些“c 什么是迭代器”的相关资讯。那么小编在网络上汇集了一些有关“c 什么是迭代器””的相关资讯,希望兄弟们能喜欢,你们一起来了解一下吧!

接着上一篇文章《C#核心-迭代器揭秘1》,没看的朋友请先看一下。

现在我们想一下,既然各种版本的C#生成的IL语言差不多,那么我们直接把这份代码降级到C#1.0,ILSpy工具这个功能。看看C#1.0代码是怎么样的。

图1

图2

图3

大家发现没,yield代码已经消失了,与之出现的是实现了IEnumerator的类。这个和我们第一篇举的例子是一样的。遍历过程中yield return执行的次数,对应这个实现了IEnumerator的类对应的状态字段记录他,请看这个"<>1_state"字段。看过我《C#核心- async await》文章的朋友,这篇文章里面提到的状态机类和这里的这个迭代器类两个类的代码是不是有点相似性?相似性就在于都记录了状态,状态机类记录的是执行了多少次异步await,这边迭代器类是记录了执行了多少次yield return。而且一个是异步迭代,而这里是同步迭代,而且都有"MoveNext"方法,我们可以好好体会体会。

上面我想说明的是yield只是编译器的一个语法糖,目的就是快速构建一个迭代器类。

我们回过头再看一下用yield实现的迭代器类

图4

图5

图6

这个写法还能再次简化。如图

图7

图8

图9

可以看到InterationSample4这个方法,没有看见实现IEnumerable的类了,同样也没有实现IEnumerator的类了。直接通过yield返回IEnumerable。根据上面所说的一样,我们把它降级,看看会怎么样。请看下图。

图10

图11

你会发现看不到yield关键字了,编译器帮我们生成实现IEnumerable,IEnumerator的类,也就是和我们之前讨论的一样了。但是现在的写法更加的简单了。

我们在第一篇问了一个问题,就是把"a","b","c","d","e",输出为"c","d","e","a","b",或者"d","e","a","b","c"为什么需要迭代器这么复杂,我直接通过一个for循环就可以做到。即使你加入一个yield,即使简化到一个方法的方式实现迭代器,也没有for循环实现得那么快,或者说看不出来差距。我们看下for怎么实现这个代码

图7

异常简单。不过我在上篇也说了我只是想要循序渐进。接下来,我在提出一个需求,就是除了让能让这个"a","b","c","d","e"原始数据能够按照我想要的顺序的基础上,将顺序变化之后的数组的每个成员成为一个类型对象的字段值,比如对象是Class1,字段是Property1。请看下图。

图8

图9

我又建了一个InterationSample5方法,用于将之前改变顺序的数据通过迭代器的方式组装成对象并输出出来。我们看一下C#里面是怎么样子的。

图10

可以看到生成了两个迭代器类。我们看一下外层迭代器的MoveNext 方法。

图11

图12

图13

查看图12代码"<>1__state=0",moveNext方法里面会递归"<InterationSample4>d__3"这个迭代器,而这个迭代器的作用就是我们之前的例子,拿到变更顺序之后的成员,然后在"<InterationSample4>d__2"这个迭代器里面把结果拿到组装成对象返回。这里迭代器的嵌套,其实就是外层迭代器调用MoveNext方法,然后MoveNext方法调用内部迭代器的MoveNext方法,拿到数据处理,然后返回。就像我们第一篇说的那句话一样。"构建出一个数据管道,把源头数据通过一系列的转换和过滤变成你想要的数据"。这个过程就像把很多苹果放到机器猫的口袋里面,伸手进去,一个一个拿出来,经过了几道工序,拿出来的时候发现,苹果变成削去了皮的苹果,然后又变成了苹果泥,最终拿出来的时候已经是包装好的苹果酱了。

重点是一个一个的顺序,直到口袋里面没有苹果了,我们可以看到外层迭代器,foreach的是IEnumerable<Class1>,我们需要知道他成员数量吗,这里我们知道成员数量,因为数组是我们传进去的。但是如果这个数组不是传进去的,而是和下图这样。

图14

图15

外层并不知道成员数量,所以不能把IEnumerable<Class1>当做一个数据结构,因为数据结构往往我们可以知道有多少个成员,有Count,或者Length属性,而这个只能看做序列,不知道成员数量,只能通过foreach一个一个获取数据。上面这个例子我们也没有拿到排序后的数据"d","e","a","b","c",这只是一个中间状态。

我们再来看一下InterationSample5的返回值,IEnumerable<Class1>。这个是泛型迭代器,这个也很简单,我们看下实现就行了。

图16

图17

也就是如果要返回泛型迭代器,那么迭代器除了要实现IEnumerable,还要实现IEnumerable<Class>,这两个接口里面都有Current和MoveNext()方法。

我们上面两个方法嵌套的迭代器实现起来依然不方便,而且通过一个for循环也能完成我们想要做的事情。我们只要在for循环里面调用数据处理的方法(自己封装一下) 不就行了。

但是我们回想一下,每次迭代器都是返回一个IEnumerable<Class>,这个是共性,我们能不能封装一个方法,让迭代器递归调用更加的方便,肯定可以!请看下图。

图18

为什么这么写,我就是想着迭代器的目的,还是那句话"构建出一个数据管道,把源头数据通过一系列的转换和过滤变成你想要的数据"。那我们想一下其实就是一个数据,多次通过一道工序,转成另外一个数据。请看图18,输入IEnumerable<T> enumerable,就是工序的初始序列,Func<T,R> func,就是工序。我们迭代原始数据,用我们传入的委托执行这道工序,返回我们要的目标序列,也就是IEnumerable<R>。好了,我们改一下我们之前的代码。

图19

图20

图21

图22

我们将原始数据,第一步改变顺序,第二步封装到Class1类,第三步,给Class1类Level字段赋值,第四步,把Class1类分装到Class2类里面。

按照我们之前的写的方式,需要写4个方法,然后迭代,而且先调用最后的工序,然后里面调用前一个工序,看起来非常不直观。而现在呢。

图23

这样我们是先拿到原始数据,然后通过封装的扩展方法InterationSample4和Select方法进行一步一步的调用,最终得到我们想要的序列,这个过程非常直观,工序是正序执行而非倒序,代码也很少。这种调用方式叫做"链式编程思想",大家有没有用过前端的jquery,也是这种编程。完全符合人的思维方式,就是每一步都看得懂,换一个人来看也很快能看懂。

再扩散想一想,如果有10道工序,都是处理这些数据的,那么只要一直使用Select的方法就行了。用for循环去做有那么直观吗?再扩散想一下,如果还要从图23里面的第二步也需要走别的工序而不是走3,4工序,走别的工序,是不是只要将2的结果IEnumerable<Class1>放在一个变量里面,然后一边走3,4工序,另外一个走5,6工序就行了。如下图所示。

图24

如果for循环的话是不是得写很多代码,很多重复的代码,而且没有图24清楚。

不知道大家有没有用过ef或者sqlsugar,他们用的时候就是和上面说的一样的思想,"链式编程思想"。给大家看一下ef使用的代码。

图25

图26

这是一个查询一段时间内的相关数据报表接口,都是这种目的很清楚的链式编程,看图26,我就写了一个迭代器,返回一段时间的序列,很多报表都有这个一段时间内的数据报表。很多人偏向每个报表里面先写for循环,拿到日期,然后再做后面处理,我这里直接返回序列,后面就非常清楚了。看看ef写法,其实内部都是扩展方法,迭代器,委托。这些也是linq的重要实现手段。

最重要的是我们得了解清楚他的内部是怎么运行的,寥寥几句代码,但是背后编译器为我们生成了非常多的迭代器相关的代码。现在大家会体现到我第一篇说的那个话了吧,就是迭代器可能我们每天都在用,只是我们不自知。

标签: #c 什么是迭代器