迭代器
您撰寫的幾乎所有程式或多或少都需要逐一查看集合。 您將會撰寫程式碼,以查看集合中的每個項目。
您也會建立迭代器方法,也就是為該類別的元素產生「迭代器」的方法。 「迭代器」是周遊容器的物件,特別是清單。 迭代器可以用於:
- 在集合中的每個項目上執行動作。
- 列舉自訂集合。
- 擴充 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
陳述式會為您處理上述所有細微差異。 編譯器會為上述任何建構產生正確的程式碼。