從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()
測試WithYield()
測試2:
測試WithNoYield()
測試WithYield()
給你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()方法:
暈,怎么搞的,這是我寫的代碼么?我的for循環呢?經過我再三確認,確實是我寫的代碼生成的。我心里暗暗叫罵,編譯器,你怎么能這樣“無恥”,在背后修改我的代碼,你這不侵權么。還給我新生成了一個類<WithYield>d__0,這個類實現了這么幾個接口:IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable(好啊,這個類將枚舉接口和迭代器接口都實現了)
現在能解答測試1為什么沒有輸出了,調用WithYield()里面就是調用了一下<WithYield>d__0的構造方法,<WithYield>d__0的構造方法的代碼:
這里沒有任何輸出。
在測試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,這個我們平常經常使用的東西居然背后還隱藏著這么多奇妙的地方,我也是今天才知道,看來未來的路還很遠很遠啊。
路漫漫其修遠兮,吾將上下而求索。
新聞熱點
疑難解答