您撰寫的每個程式幾乎都需要迭代集合。 您將撰寫程式碼來檢查集合中的每個項目。
您也會建立反覆運算器方法,也就是產生該類別元素之 反覆運算器 的方法。 迭代器是遍歷容器的物件,特別是列表。 反覆運算器可用於:
- 對集合中的每個項目執行動作。
- 列舉自定義集合。
- 擴充 LINQ 或其他函式庫。
- 建立資料管線,使資料能透過迭代方法高效流動。
C# 語言提供產生和取用序列的功能。 這些序列可以同步或異步產生和取用。 本文提供這些功能的概觀。
使用 foreach 進行反覆運算
列舉集合很簡單: foreach
關鍵詞會列舉集合,針對集合中的每個項目執行一次內嵌語句:
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
就這樣。 若要迭代遍歷集合的所有內容,只需使用foreach
語句即可。 不過,語句 foreach
不是神奇的。 它依賴 .NET Core 連結庫中定義的兩個泛型介面,以產生反覆運算集合所需的程式代碼: IEnumerable<T>
和 IEnumerator<T>
。 以下會更詳細地說明此機制。
這兩個介面也有非泛型對應專案: IEnumerable
和 IEnumerator
。 現代程式碼偏好使用通用版本。
以異步方式產生序列時,您可以使用 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
的型別無法隱含轉換到IDisposable
或IAsyncDisposable
,finally
子句就會展開為空區塊:
finally
{
}
如果存在從enumerator
類型到IDisposable
的隱含轉換,而且enumerator
是不可為 Null 的實值型別,finally
條件句會展開為:
finally
{
((IDisposable)enumerator).Dispose();
}
謝天謝地,您不需要記住所有這些詳細數據。
foreach
語句會為您處理所有這些細微差別。 編譯程式會針對任何這些建構產生正確的程序代碼。