題目:從0遍歷到20(不包括20),輸出遍歷到的每個元素,並將大於2的所有 數字放到一個IEnumerable中返回
解答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分鐘時間給出答案 ,不要上機運行
測試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循環呢?經過我再三確認,確實是我寫的代 碼生成的。我心裡暗暗叫罵,編譯器,你怎麼能這樣“無恥”,在背 後修改我的代碼,你這不侵權麼。還給我新生成了一個類d__0,這個類實現了這 麼幾個接口:IEnumerable, IEnumerable, IEnumerator, IEnumerator, IDisposable(好啊,這個類將枚舉接口和迭代器接口都實現了)現在能解答測 試1為什麼沒有輸出了,調用WithYield()裡面就是調用了一下d__0的構造方法, d__0的構造方法的代碼:
public <WithYield>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
}
這裡沒有任何輸出。
在測試2中,首先我們會 調用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接口的類(在Linq裡,所有實現了IEnumerable接口的類都被稱作 sequence),對它的foreach或者while的訪問必須通過它對應的IEnumerator的 MoveNext()方法,如果我們把一些耗時的或者需要延遲的操作放在MoveNext()裡 面,那麼只有等到MoveNext()被訪問,也就是result被使用的時候那些操作才會 執行,而給result賦值啊,傳遞啊,什麼的,那些耗時的操作都沒有被執行。
如果上面這段代碼,最後由於state小於0,而對result沒有任何需求了 ,在Linq裡返回的結果都是IEnumerable的,如果這裡沒有使用延遲計算,那那 個Linq表達式不就白運算了麼?如果是Linq to Objects還稍微好點,如果是 Linq to SQL,而且那個數據庫表又很大,真是得不償失啊,所以微軟想到了這 點,這裡使用了延遲計算,只有等到程序別的地方使用了result才會計算這裡的 Linq表達式的值的,這樣Linq的性能也比以前提高了不少,而且Linq to SQL最 後還是要生成SQL語句的,對於SQL語句的生成來說,如果將生成延遲,那麼一些 條件就先確定好了,生成SQL語句的時候就可以更精練了。還有,由於MoveNext ()是一步步執行的,循環一次執行一次,所以如果有這種情況:我們遍歷一次判 斷一下,不滿足我們的條件了我們就退出,如果有一萬個元素需要遍歷,當遍歷 到第二個的時候就不滿足條件了,這個時候我們就可就此退出,後面那麼多元素 實際上都沒處理呢,那些元素也沒有被加載到內存中來。
延遲計算還有 很多惟妙惟肖的特質,也許以後你也可以按照這種方式來編程了呢。寫到這裡我 突然想到了Command模式,Command模式將方法封裝成類,Command對象在傳遞等 時候是不會執行任何東西的,只有調用它內部那個方法他才會執行,這樣我們就 可以把命令到處發,還可以壓棧啊等等而不擔心在傳遞過程中Command被處理了 ,也許這也算是一種延遲計算吧。
本文也只是很淺的談了一下延遲計算 的東西,從這裡還可以牽扯到並發編程模型和協同程序等更多內容,由於本人才 疏學淺,所以只能介紹到這個地步了,上面一些說法也是我個人理解,肯定有很 多不妥地方,歡迎大家拍磚。
foreach,yield,這個我們平常經常使用的 東西居然背後還隱藏著這麼多奇妙的地方,我也是今天才知道,看來未來的路還 很遠很遠啊。
路漫漫其修遠兮,吾將上下而求索。
祝大家編程愉快