2010-04-29 160 views
6

所以,這個問題只是問的SO:有人可以解釋這個懶惰的評估代碼嗎?

How to handle an "infinite" IEnumerable?

我的示例代碼:

public static void Main(string[] args) 
{ 
    foreach (var item in Numbers().Take(10)) 
     Console.WriteLine(item); 
    Console.ReadKey(); 
} 

public static IEnumerable<int> Numbers() 
{ 
    int x = 0; 
    while (true) 
     yield return x++; 
} 

是否有人可以解釋爲什麼這是懶惰的評估?我在Reflector中查了這段代碼,而且比起我開始時我更困惑。

反射器輸出:

public static IEnumerable<int> Numbers() 
{ 
    return new <Numbers>d__0(-2); 
} 

對於數字方法,並且看起來已經產生了一種新型該表達式:

[DebuggerHidden] 
public <Numbers>d__0(int <>1__state) 
{ 
    this.<>1__state = <>1__state; 
    this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId; 
} 

這使得沒有意義的我。我會認爲這是一個無限循環,直到我將這些代碼放在一起並自己執行它。所以我現在明白了.Take()可以告訴foreach,枚舉已經「結束」,但實際上它沒有,但是不應該在鏈接完成之前調用Numbers()轉到Take()?採取的結果是什麼實際上正在枚舉,正確的?但是,如果Numbers沒有完全評估,那麼執行如何執行?

EDIT2:那麼這只是'yield'關鍵字執行的特定編譯器技巧嗎?

回答

1

這不是一個無限循環的原因是根據使用Linq的Take(10)調用,你只能枚舉10次。現在,如果你寫的代碼是這樣的:

foreach (var item in Numbers()) 
{ 
} 

現在這是一個無限循環,因爲您的枚舉器將始終返回一個新值。 C#編譯器獲取這些代碼並將其轉換爲狀態機。如果您的枚舉器沒有警衛子句來破壞執行,那麼調用者必須在您的示例中執行它。

代碼懶惰的原因也是代碼工作的原因。從本質上講,返回第一個項目,然後你的應用程序消耗,然後它需要另一個,直到它有10個項目。

編輯

這實際上無關與加拿的。這些被稱爲迭代器。 C#編譯器對您的代碼執行復雜的轉換,從您的方法中創建一個枚舉器。我建議閱讀它,但基本上(這可能不是100%準確),您的代碼將輸入Numbers方法,您可以設想爲初始化狀態機。

一旦你的代碼達到收益率回報,你基本上說Numbers()停止執行了,讓他們回來這個結果,然後當他們要求下一個項目在收益率返回後的下一行繼續執行。

Erik Lippert has a great series上迭代

+0

是的,但是爲了繼續進行接聽呼叫,Numbers()本身不必進行全面評估嗎?我知道數字本身是一個無限循環。爲什麼添加.Take()會突然停止Numbers的整體評估? – Tejs 2010-04-29 19:24:25

+0

有趣。感謝您的鏈接!這個問題(和代碼)讓我做了雙重考慮,現在我意識到我還有更多要學習Enumerables! – Tejs 2010-04-29 19:37:40

2

的其它方面,這必須與:

  • 的IEnumerable做什麼時,某些方法被稱爲
  • 枚舉的性質和產量聲明

當您枚舉任何類型的IEnumerable時,該類將爲您提供下一個它會給你的東西。它不會對全部做任何事情,它只是給你下一個項目。它決定該項目將會是什麼。 (例如,某些集合是有序的,有些不是集合的,有些集合不保證特定的順序,但似乎總是按照您放入的順序將其返回)。

了IEnumerable擴展方法Take()將枚舉10次,得到的前10個項目。你可以做Take(100000000),它會給你很多數字。但你只是在做Take(10)。它只是要求Numbers()下一個項目。 。 。 10倍。

每個那些10項,Numbers給出了一個項目。要理解如何,您需要閱讀Yield語句。更復雜的是syntactic sugarYield非常強大。 (我是一個VB開發人員,非常惱火,我仍然沒有它。)這不是一個函數;這是一個有一定限制的關鍵字。而且它使得定義一個枚舉器比其他方法更容易。

其他IEnumerable擴展方法總是遍歷每一個項目。調用.AsList會炸掉它。使用它大部分的LINQ查詢會炸掉它。

+0

使用yield來編寫自定義枚舉器非常有趣。 – JoshBerke 2010-04-29 19:24:26

0

基本上,你的Numbers()函數創建一個枚舉器。
foreach將在每次迭代中檢查enumrator是否已達到結尾,如果沒有,它將繼續。你真正的恩將永遠不會結束,但那並不重要。這是懶惰評估。
枚舉器將生成​​結果「生活」。
這意味着如果你想寫(3)在那裏,循環只執行三次。枚舉器仍然會有一些「剩餘」的項目,但是它們不會生成,因爲目前沒有方法需要它們。
如果你想嘗試從0到無窮大的所有數字,就像函數所暗示的那樣,並且一次返回所有的數字,那麼這個只使用10個數字的程序會慢很多。這是懶惰評估的好處 - 從未使用過的東西永遠不會計算。