初次使用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里面都有详细的讲解,感觉没有什么好多说的。