你的位置:首页 > ASP.net教程

[ASP.net教程]初次使用C#中的yield


  这几天在Python程序员的微信订阅号中总是见到yield的关键字,才想起来在C#中也是有yield,但是只是知道有,从来没有了解过他的用法,今天有时间就来看看是怎么使用的。刚开始肯定就是搜索一下用法了,找到两篇说明示例,一是 C# 中的"yield"使用,第二个是MSDN的官方api yield(C# 参考)

说实话第一个示例看完还是很模糊的概念,例子也没有看懂是在干嘛,一直到MSDN中给出结果集我才明白了到底的用法是怎么样的。

先来举例一个需求: 一个方法返回一个IEnumerable 类型结果集(例如返回一个list<int>的结果),通常的代码是这样的

 

 1     /// <summary> 2     ///  3     /// </summary> 4     /// <returns></returns> 5     public IEnumerable<int> Method() 6     { 7       List<int> results = new List<int>(); 8       int counter = 0; 9       int result = 1;10 11       while (counter++ < 10)12       {13         result = result * 2;14         results.Add(result);15       }16       return results;17     }

这样就完成了需求。

但是 有了yield之后就可以这样写了

 1     /// <summary> 2     ///  3     /// </summary> 4     /// <returns></returns> 5     public IEnumerable<int> YieldDemo() 6     { 7       int counter = 0; 8       int result = 1; 9       while (counter++ < 10)10       {11         result = result * 2;       12         yield return result;13       }14     }

两种效果是一样的,但是从我个人而言我喜欢第二个,感觉更简洁一些。

题外话:在写这两个例子中我又增加了一个知识点

  返回值IEnumerable其实和IEnumerable<object>是等价的,只是IEnumerable的结果需要动态的解析; 

 

但是还搞不清楚这两者实现有什么区别,所以我想看看这两个在做同一件事的时候效率如何,下面来尝试使用while循环10000000的取数据的耗时比较

使用 BenchmarkDotNet  测试结果如下

使用Stopwatch测试结果也是一样的

从这个测试里面可以看出YieldDemo方法几乎没有耗时,但是实际情况是不可能的吧,所以我又尝试做了遍历的测试

 

 1       Stopwatch stop = new Stopwatch(); 2       stop.Start(); 3       var res = new YieldTest().YieldDemo(); 4       foreach (var item in res) 5       { 6  7       } 8       var a = stop.ElapsedMilliseconds; 9       stop.Restart();10 11 12       var rrrrr = new YieldTest().Method();13       foreach (var item in rrrrr)14       {15 16       }17       var b = stop.ElapsedMilliseconds;18       stop.Restart();

这个测试的结果是a=168,b=142.对比上一个测试结果让我更加疑惑,我就开始打断点,看看执行的顺序是怎样的。

结果如下:  

     在 第三行 断点压根就没有进YieldDemo这个方法,而是当进行foreach 遍历结果的时候,才开始进入了YieldDemo这个方法,更奇怪的是每次的foreach 都会进入YieldDemo的while一次去取数据

这个结果让我有点懵了,只能再仔细看看文档解析,

  迭代器方法运行到 yield return 语句时,会返回一个 expression,并保留当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。 可以使用 yield break 语句来终止迭代。 

貌似这里面是涉及到了迭代器的东西。马上找迭代器的知识点,在 详解C# 迭代器 中看到这样一句解释

  需要强调的一点是,对于迭代块,虽然我们写的方法看起来像是在顺序执行,实际上我们是让编译器来为我们创建了一个状态机。这就是在C#1中我们书写的那部分代码---调用者每次调用只需要返回一个值,因此我们需要记住最后一次返回值时,在集合中位置。

  当编译器遇到迭代块时,它创建了一个实现了状态机的内部类。这个类记住了我们迭代器的准确当前位置以及本地变量,包括参数。

这句话貌似解析了上面的疑问,但是看的有点云里雾里,还要花时间消化一下里面的具体原理 。

官方提示使用yield有一些限制,需要注意

1 不能将 yield return 语句置于 try-catch 块中。 可将 yield return 语句置于 try-finally 语句的 try 块中。2 可将 yield break 语句置于 try 块或 catch 块中,但不能将其置于 finally 块中。3 如果 foreach 主体(在迭代器方法之外)引发异常,则将执行迭代器方法中的 finally 块。4 匿名方法。 有关详细信息,请参阅匿名方法。5 包含不安全的块的方法。 有关详细信息,请参阅unsafe。

 

针对第一点让我感觉使用好有限制,为啥不能在try-catch 中使用呢?

关于其他的一些使用方法在MSDN里面都有详细的讲解,感觉没有什么好多说的。