共用方式為


迭代器

您撰寫的每個程式幾乎都需要迭代集合。 您將撰寫程式碼來檢查集合中的每個項目。

您也會建立反覆運算器方法,也就是產生該類別元素之 反覆運算器 的方法。 迭代器是遍歷容器的物件,特別是列表。 反覆運算器可用於:

  • 對集合中的每個項目執行動作。
  • 列舉自定義集合。
  • 擴充 LINQ 或其他函式庫。
  • 建立資料管線,使資料能透過迭代方法高效流動。

C# 語言提供產生和取用序列的功能。 這些序列可以同步或異步產生和取用。 本文提供這些功能的概觀。

使用 foreach 進行反覆運算

列舉集合很簡單: foreach 關鍵詞會列舉集合,針對集合中的每個項目執行一次內嵌語句:

foreach (var item in collection)
{
    Console.WriteLine(item?.ToString());
}

就這樣。 若要迭代遍歷集合的所有內容,只需使用foreach語句即可。 不過,語句 foreach 不是神奇的。 它依賴 .NET Core 連結庫中定義的兩個泛型介面,以產生反覆運算集合所需的程式代碼: IEnumerable<T>IEnumerator<T>。 以下會更詳細地說明此機制。

這兩個介面也有非泛型對應專案: IEnumerableIEnumerator。 現代程式碼偏好使用通用版本。

以異步方式產生序列時,您可以使用 await foreach 語句以異步方式取用序列:

await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}

當序列是 System.Collections.Generic.IEnumerable<T>時,您可以使用 foreach。 當序列是 System.Collections.Generic.IAsyncEnumerable<T>時,您可以使用 await foreach。 在後者的情況下,會以異步方式產生序列。

使用迭代器方法枚舉來源

C# 語言的另一個絕佳功能可讓您建置方法,以建立列舉的來源。 這些方法稱為 反覆運算器方法。 反覆運算器方法會定義如何在要求時產生序列中的物件。 您可以使用 yield return 內容關鍵詞來定義反覆運算器方法。

您可以撰寫此方法,以產生從 0 到 9 的整數序列:

public IEnumerable<int> GetSingleDigitNumbers()
{
    yield return 0;
    yield return 1;
    yield return 2;
    yield return 3;
    yield return 4;
    yield return 5;
    yield return 6;
    yield return 7;
    yield return 8;
    yield return 9;
}

上述程式代碼顯示不同的 yield return 語句,以反白顯示您可以在反覆運算器方法中使用多個離散 yield return 語句的事實。 您可以使用其他語言建構來簡化反覆運算器方法的程式代碼。 下列方法定義會產生完全相同的數位序列:

public IEnumerable<int> GetSingleDigitNumbersLoop()
{
    int index = 0;
    while (index < 10)
        yield return index++;
}

您不必決定一個或另一個。 您可以視需要有盡可能多的 yield return 語句,以符合方法的需求:

public IEnumerable<int> GetSetsOfNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    index = 100;
    while (index < 110)
        yield return index++;
}

上述所有範例都會有異步對應項目。 在每個案例中,您都會將 的 IEnumerable<T> 傳回型別取代為 IAsyncEnumerable<T>。 例如,上一個範例會有下列異步版本:

public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    await Task.Delay(500);

    yield return 50;

    await Task.Delay(500);

    index = 100;
    while (index < 110)
        yield return index++;
}

這是同步和異步反覆運算器的語法。 讓我們考慮一個真實世界的範例。 假設您位於IoT專案上,而裝置感測器會產生非常大的數據串流。 若要了解數據,您可以撰寫一個方法,以取樣每個第 N 個數據元素。 這個小型反覆運算器方法會執行下列動作:

public static IEnumerable<T> Sample<T>(this IEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

如果從 IoT 裝置讀取會產生異步序列,您會修改 方法,如下列方法所示:

public static async IAsyncEnumerable<T> Sample<T>(this IAsyncEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    await foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

反覆運算器方法有一個重要的限制:您無法在相同的方法中同時有 return 語句和 yield return 語句。 下列程序代碼不會編譯:

public IEnumerable<int> GetSingleDigitNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    // generates a compile time error:
    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    return items;
}

這項限制通常不是問題。 您可以選擇在整個方法中使用 yield return ,或者將原始方法分隔成多個方法,其中一些使用 return ,而另一些則使用 yield return

您可以稍微修改最後一個方法,以在任何地方使用 yield return

public IEnumerable<int> GetFirstDecile()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    foreach (var item in items)
        yield return item;
}

有時候,正確的答案是將反覆運算器方法分割成兩個不同的方法。 一個使用 return,另一個使用 yield return。 請考慮您可能想要根據布爾自變數傳回空集合或前五個奇數的情況。 您可以將內容表達為以下兩種方法:

public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)
{
    if (getCollection == false)
        return new int[0];
    else
        return IteratorMethod();
}

private IEnumerable<int> IteratorMethod()
{
    int index = 0;
    while (index < 10)
    {
        if (index % 2 == 1)
            yield return index;
        index++;
    }
}

查看上述方法。 第一個會使用標準 return 語句傳回空集合,或第二個方法所建立的反覆運算器。 第二個方法使用yield return語句來建立所需的序列。

深入探討 foreach

foreach 語句擴充成一種標準語式,使用 IEnumerable<T>IEnumerator<T> 介面以便反覆運算集合中所有元素。 它也會讓開發人員無法正確管理資源所發生的錯誤降到最低。

編譯程式會將 foreach 第一個範例中顯示的循環轉譯為類似此建構的迴圈:

IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
    var item = enumerator.Current;
    Console.WriteLine(item.ToString());
}

編譯器生成的程式碼更加複雜,並能處理由 GetEnumerator() 所傳回的物件實現 IDisposable 介面的情況。 完整擴充會產生更像這樣的程式代碼:

{
    var enumerator = collection.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of enumerator.
    }
}

編譯程式會將第一個異步範例轉譯成類似此建構的內容:

{
    var enumerator = collection.GetAsyncEnumerator();
    try
    {
        while (await enumerator.MoveNextAsync())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of async enumerator.
    }
}

列舉器的處置方式取決於enumerator型別的特性。 在一般同步案例中,finally 子句會展開為:

finally
{
   (enumerator as IDisposable)?.Dispose();
}

一般異步案例會展開為:

finally
{
    if (enumerator is IAsyncDisposable asyncDisposable)
        await asyncDisposable.DisposeAsync();
}

不過,如果enumerator的型別是密封類型,且enumerator的型別無法隱含轉換到IDisposableIAsyncDisposablefinally子句就會展開為空區塊:

finally
{
}

如果存在從enumerator類型到IDisposable的隱含轉換,而且enumerator是不可為 Null 的實值型別,finally條件句會展開為:

finally
{
   ((IDisposable)enumerator).Dispose();
}

謝天謝地,您不需要記住所有這些詳細數據。 foreach語句會為您處理所有這些細微差別。 編譯程式會針對任何這些建構產生正確的程序代碼。