簡體   English   中英

在C#中使用yield return迭代器的目的/優勢是什么?

[英]What is the purpose/advantage of using yield return iterators in C#?

我看到的所有使用yield return x;的例子yield return x; 只需返回整個列表,就可以以相同的方式完成C#方法。 在這些情況下,使用yield return語法與返回列表是否有任何好處或優勢?

此外,在什么類型的場景中會yield return ,你不能只返回完整的列表?

但是如果你自己建造一個系列呢?

通常,迭代器可用於延遲生成一系列對象 例如, Enumerable.Range方法內部沒有任何類型的集合。 它只是按需生成下一個數字。 使用狀態機生成這種延遲序列有很多用途。 其中大多數都涵蓋在函數式編程概念中

在我看來,如果你把迭代器看作是枚舉集合的一種方式(它只是最簡單的用例之一),那你就走錯了路。 正如我所說,迭代器是返回序列的手段。 序列甚至可能是無限的 無法返回無限長度的列表並使用前100個項目。 偷懶的時候。 返回集合與返回集合生成器 (迭代器是什么) 有很大不同 它將蘋果與橙子進行比較。

假設的例子:

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

此示例打印小於10000的素數。您可以輕松地將其更改為打印少於一百萬的數字,而無需觸及素數生成算法。 在此示例中,您不能返回所有素數的列表,因為序列是無限的,並且消費者甚至不知道它從一開始就想要多少項。

這里的好答案表明,收益yield return的好處是你不需要創建一個列表 ; 列表可能很昂貴。 (此外,過了一會兒,你會發現它們笨重而且不夠優雅。)

但是如果你沒有List怎么辦?

yield return允許您以多種方式遍歷數據結構 (不一定是列表)。 例如,如果您的對象是樹,則可以按前或后順序遍歷節點,而無需創建其他列表或更改基礎數據結構。

public IEnumerable<T> InOrder()
{
    foreach (T k in kids)
        foreach (T n in k.InOrder())
            yield return n;
    yield return (T) this;
}

public IEnumerable<T> PreOrder()
{
    yield return (T) this;
    foreach (T k in kids)
        foreach (T n in k.PreOrder())
            yield return n;
}

延遲評估/延期執行

在您實際調用該特定結果之前,“yield return”迭代器塊不會執行任何代碼。 這意味着它們也可以有效地鏈接在一起。 流行測驗:以下代碼將在文件上迭代多少次?

var query = File.ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

答案恰好是一個,直到在foreach循環中向下。 即使我有三個獨立的linq運算符函數,我們仍然只循環遍歷文件的內容一次。

除性能外,這還有其他好處。 例如,我可以編寫一個簡單而通用的方法來讀取和預過濾日志文件一次,並在幾個不同的地方使用相同的方法,每次使用都會添加不同的過濾器。 因此,我保持良好的性能,同時也有效地重用代碼。

無限的名單

請參閱我對這個問題的回答,以獲得一個好例子:
C#fibonacci函數返回錯誤

基本上,我使用迭代器塊來實現斐波那契序列,該迭代器塊永遠不會停止(至少在到達MaxInt之前),然后以安全的方式使用該實現。

改進的語義和關注點分離

再次使用上面的文件示例,我們現在可以輕松地將讀取文件的代碼與從實際解析結果的代碼中過濾掉不需要的行的代碼分開。 特別是第一個是非常可重復使用的。

這是那些東西,是更難用散文來解釋一個比它究竟是誰用一個簡單的視覺1:

關注的命令與功能分離

如果您看不到圖像,則會顯示相同代碼的兩個版本,並針對不同的問題提供背景突出顯示。 linq代碼具有很好地分組的所有顏色,而傳統的命令式代碼具有混合的顏色。 作者認為(並且我同意)這個結果是使用linq與使用命令式代碼的典型結果...... linq在組織代碼方面做得更好,以便在各個部分之間獲得更好的流程。


1我相信這是最初的來源: https//twitter.com/mariofusco/status/571999216039542784 另請注意,此代碼是Java,但C#類似。

有時您需要返回的序列太大而無法放入內存中。 例如,大約3個月前,我參加了一個MS SLQ數據庫之間的數據遷移項目。 數據以XML格式導出。 對於XmlReader, 收益率回報非常有用。 它使編程變得更加容易。 例如,假設一個文件有1000個Customer元素 - 如果您只是將此文件讀入內存,則需要將所有文件同時存儲在內存中,即使它們是按順序處理的。 因此,您可以使用迭代器逐個遍歷集合。 在這種情況下,你必須為一個元素花費內存。

事實證明,對我們的項目使用XmlReader是使應用程序工作的唯一方法 - 它工作了很長時間,但至少它沒有掛起整個系統並且沒有引發OutOfMemoryException 當然,您可以使用XmlReader而不使用yield迭代器。 但是迭代器使我的生活變得更加輕松(我不會那么快地編寫導入代碼而沒有麻煩)。 觀看此頁面以了解如何使用yield迭代器來解決實際問題(不僅僅是無限序列的科學)。

在玩具/演示場景中,沒有太大的區別。 但是在某些情況下,產生迭代器是有用的 - 有時候,整個列表不可用(例如流),或者列表計算成本高,並且不可能完全需要。

如果整個列表都是巨大的,它可能會占用大量的內存而只是為了坐下來,而在產量方面,你只需要在你需要的時候玩,不管有多少項。

請看看Eric White的博客(順便說一下,優秀的博客)關於懶惰與熱切評估的討論

使用yield return您可以迭代項目而無需構建列表。 如果您不需要列表,但想要迭代某些項目,則可以更容易編寫

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

您可以使用所有邏輯來確定是否要使用yield返回操作方法中的foo,並且foreach循環可以更加簡潔。

這是我之前對完全相同問題的接受答案:

收益率關鍵字增值?

查看迭代器方法的另一種方法是,他們努力將算法“從里到外”。 考慮一個解析器。 它從流中提取文本,在其中查找模式並生成內容的高級邏輯描述。

現在,我可以通過采用SAX方法讓我自己成為解析器作者,我有一個回調接口,每當我找到下一個模式時我都會通知它。 所以在SAX的情況下,每當我找到一個元素的開頭時,我都會調用beginElement方法,依此類推。

但這給我的用戶帶來了麻煩。 他們必須實現處理程序接口,因此他們必須編寫響應回調方法的狀態機類。 這很難做到,所以最簡單的方法是使用構建DOM樹的庫存實現,然后他們將能夠方便地遍歷樹。 但隨后整個結構被緩存在內存中 - 並不好。

但是如何將我的解析器編寫為迭代器方法呢?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

這比回調接口方法更難寫 - 只需返回從我的LanguageElement基類派生的對象,而不是調用回調方法。

用戶現在可以使用foreach循環遍歷解析器的輸出,因此他們可以獲得一個非常方便的命令式編程接口。

結果是自定義API的兩面看起來都像是在控制中 ,因此更容易編寫和理解。

使用yield的基本原因是它自己生成/返回一個列表。 我們可以使用返回的列表進一步迭代。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM