欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C#特性 迭代器(下) yield以及流的延迟计算

程序员文章站 2023-12-15 09:52:58
从0遍历到20(不包括20),输出遍历到的每个元素,并将大于2的所有数字放到一个ienumerable中返回 解答1:(我以前经常这样做)...

从0遍历到20(不包括20),输出遍历到的每个元素,并将大于2的所有数字放到一个ienumerable<int>中返回

解答1:(我以前经常这样做)

static ienumerable<int> withnoyield()
    {
      ilist<int> list = new list<int>();
      for (int i = 0; i < 20; i++)
      {
        console.writeline(i.tostring());
        if(i > 2)
          list.add(i);
      }
      return list;
    }

解答2:(自从有了c# 2.0我们还可以这样做)

static ienumerable<int> withyield()
    {
      for (int i = 0; i < 20; i++)
      {
        console.writeline(i.tostring());
        if(i > 2)
          yield return i;
      }
    }

如果我用下面这样的代码测试,会得到怎样的输出?
测试1:

测试withnoyield()

复制代码 代码如下:

static void main()
        {
            withnoyield();
            console.readline();
        }

测试withyield()

复制代码 代码如下:

static void main()
        {
            withyield();
            console.readline();
        }

测试2:
测试withnoyield()

复制代码 代码如下:

static void main()
        {
            foreach (int i in withnoyield())
            {
                console.writeline(i.tostring());
            }
            console.readline();
        }

测试withyield()

复制代码 代码如下:

static void main()
        {
            foreach (int i in withyield())
            {
                console.writeline(i.tostring());
            }
            console.readline();
        }

给你5分钟时间给出答案,不要上机运行

*********************************5分钟后***************************************

测试1的运算结果
测试withnoyield():输出从0-19的数字
测试withyield():什么都不输出
测试2的运算结果
测试withnoyield():输出1-19接着输出3-19
测试withyield():输出12334455…….
(为节省空间上面的答案没有原样粘贴,可以自己运行测试)

 

是不是感到很奇怪,为什么使用了yield的程序表现的如此怪异呢?

测试1中对withyield()的测试,明明方法调用了,居然一行输出都没有,难道for循环根本没有执行?通过断点调试果然如此,for循环根本没有进去,这是咋回事?测试2中对withyield()的测试输出是有了,不过输出怎么这么有趣?穿插着输出,在foreach遍历withyield()的结果的时候,好像不等到最后一条遍历完,withyield()不退出,这又是怎么回事?

还是打开il代码瞧一瞧到底发生了什么吧

main方法的il代码:

.method private hidebysig static void main() cil managed
{
  .entrypoint
  .maxstack 1
  .locals init (
    [0] int32 i,
    [1] class [mscorlib]system.collections.generic.ienumerator`1<int32> cs$5$0000)
  l_0000: call class [mscorlib]system.collections.generic.ienumerable`1<int32> testlambda.program::withyield()
  l_0005: callvirt instance class [mscorlib]system.collections.generic.ienumerator`1<!0> [mscorlib]system.collections.generic.ienumerable`1<int32>::getenumerator()
  l_000a: stloc.1 
  l_000b: br.s l_0020
  l_000d: ldloc.1 
  l_000e: callvirt instance !0 [mscorlib]system.collections.generic.ienumerator`1<int32>::get_current()
  l_0013: stloc.0 
  l_0014: ldloca.s i
  l_0016: call instance string [mscorlib]system.int32::tostring()
  l_001b: call void [mscorlib]system.console::writeline(string)
  l_0020: ldloc.1 
  l_0021: callvirt instance bool [mscorlib]system.collections.ienumerator::movenext()
  l_0026: brtrue.s l_000d
  l_0028: leave.s l_0034
  l_002a: ldloc.1 
  l_002b: brfalse.s l_0033
  l_002d: ldloc.1 
  l_002e: callvirt instance void [mscorlib]system.idisposable::dispose()
  l_0033: endfinally 
  l_0034: call string [mscorlib]system.console::readline()
  l_0039: pop 
  l_003a: ret 
  .try l_000b to l_002a finally handler l_002a to l_0034
}

这里没什么稀奇的,在上一篇我已经分析过了,foreach内部就是转换成调用迭代器的movenext()方法进行while循环。我浏览到withyield()方法:

复制代码 代码如下:

private static ienumerable<int> withyield()
{
    return new <withyield>d__0(-2);
}

晕,怎么搞的,这是我写的代码么?我的for循环呢?经过我再三确认,确实是我写的代码生成的。我心里暗暗叫骂,编译器,你怎么能这样“无耻”,在背后修改我的代码,你这不侵权么。还给我新生成了一个类<withyield>d__0,这个类实现了这么几个接口:ienumerable<int>, ienumerable, ienumerator<int>, ienumerator, idisposable(好啊,这个类将枚举接口和迭代器接口都实现了)
现在能解答测试1为什么没有输出了,调用withyield()里面就是调用了一下<withyield>d__0的构造方法,<withyield>d__0的构造方法的代码:

复制代码 代码如下:

public <withyield>d__0(int <>1__state)
    {
        this.<>1__state = <>1__state;
        this.<>l__initialthreadid = thread.currentthread.managedthreadid;
    }

这里没有任何输出。
在测试2中,首先我们会调用<withyield>d__0的getenumerator()方法,这个方法里将一个整型局部变量<>1__state初始化为0,再看看movenext()方法的代码:

private bool movenext()
  {
    switch (this.<>1__state)
    {
      case 0:
        this.<>1__state = -1;
        this.<i>5__1 = 0;
        goto label_006a;

      case 1:
        this.<>1__state = -1;
        goto label_005c;

      default:
        goto label_0074;
    }
  label_005c:
    this.<i>5__1++;
  label_006a:
    if (this.<i>5__1 < 20)
    {
      console.writeline(this.<i>5__1.tostring());
      if (this.<i>5__1 > 2)
      {
        this.<>2__current = this.<i>5__1;
        this.<>1__state = 1;
        return true;
      }
      goto label_005c;
    }
  label_0074:
    return false;
  }

原来我们for循环里面的console.writeline跑到这里来了,所以没等到movenext()调用,for里面的输出也是不会被执行的,因为每次遍历都要访问movenext()方法,所以没有等到返回结果里面的元素遍历完withyield()也是不会退出的。现在我们的测试程序所表现出来的怪异行为是可以找到依据了,那就是:编译器在后台搞了鬼。

实际上这种实现在理论上是有支撑的:延迟计算(lazy evaluation或delayed evaluation)在wiki上可以找到它的解释:将计算延迟,直到需要这个计算的结果的时候才计算,这样就可以因为避免一些不必要的计算而改进性能,在合成一些表达式时候还可以避免一些不必要的条件,因为这个时候其他计算都已经完成了,所有的条件都已经明确了,有的根本不可达的条件可以不用管了。反正就是好处很多了。

延迟计算来源自函数式编程,在函数式编程里,将函数作为参数来传递,你想呀,如果这个函数一传递就被计算了,那还搞什么搞,如果你使用了延迟计算,表达式在没有使用的时候是不会被计算的,比如有这样一个应用:x=expression,将这个表达式赋给x变量,但是如果x没有在别的地方使用的话这个表达式是不会被计算的,在这之前x里装的是这个表达式。

看来这个延迟计算真是个好东西,别担心,整个linq就是建立在这之上的,这个延迟计算可是帮了linq的大忙啊(难道在2.0的时候,微软就为它的linq开始蓄谋了?),看下面的代码:

var result = from book in books
  where book.title.startwiths(“t”)
  select book
if(state > 0)
{
  foreach(var item in result)
  {
    //….
}
}

result是一个实现了ienumerable<t>接口的类(在linq里,所有实现了ienumerable<t>接口的类都被称作sequence),对它的foreach或者while的访问必须通过它对应的ienumerator<t>的movenext()方法,如果我们把一些耗时的或者需要延迟的操作放在movenext()里面,那么只有等到movenext()被访问,也就是result被使用的时候那些操作才会执行,而给result赋值啊,传递啊,什么的,那些耗时的操作都没有被执行。

如果上面这段代码,最后由于state小于0,而对result没有任何需求了,在linq里返回的结果都是ienumerable<t>的,如果这里没有使用延迟计算,那那个linq表达式不就白运算了么?如果是linq to objects还稍微好点,如果是linq to sql,而且那个数据库表又很大,真是得不偿失啊,所以微软想到了这点,这里使用了延迟计算,只有等到程序别的地方使用了result才会计算这里的linq表达式的值的,这样linq的性能也比以前提高了不少,而且linq to sql最后还是要生成sql语句的,对于sql语句的生成来说,如果将生成延迟,那么一些条件就先确定好了,生成sql语句的时候就可以更精练了。还有,由于movenext()是一步步执行的,循环一次执行一次,所以如果有这种情况:我们遍历一次判断一下,不满足我们的条件了我们就退出,如果有一万个元素需要遍历,当遍历到第二个的时候就不满足条件了,这个时候我们就可就此退出,后面那么多元素实际上都没处理呢,那些元素也没有被加载到内存中来。

延迟计算还有很多惟妙惟肖的特质,也许以后你也可以按照这种方式来编程了呢。写到这里我突然想到了command模式,command模式将方法封装成类,command对象在传递等时候是不会执行任何东西的,只有调用它内部那个方法他才会执行,这样我们就可以把命令到处发,还可以压栈啊等等而不担心在传递过程中command被处理了,也许这也算是一种延迟计算吧。

本文也只是很浅的谈了一下延迟计算的东西,从这里还可以牵扯到并发编程模型和协同程序等更多内容,由于本人才疏学浅,所以只能介绍到这个地步了,上面一些说法也是我个人理解,肯定有很多不妥地方,欢迎大家拍砖。

foreach,yield,这个我们平常经常使用的东西居然背后还隐藏着这么多奇妙的地方,我也是今天才知道,看来未来的路还很远很远啊。

路漫漫其修远兮,吾将上下而求索。

上一篇:

下一篇: