這幾天在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裡面都有詳細的講解,感覺沒有什麼好多說的。