為什麼需要 yield?
在 C# 開發中,處理大量資料的集合是常見需求。傳統做法是先把所有資料載入記憶體,再逐一處理——這在資料量大時會造成明顯的記憶體壓力,甚至 OOM。
yield 關鍵字讓方法能實現延遲執行(Lazy Evaluation):呼叫端每次透過 foreach 取值,迭代器方法才往前跑一步、吐出一個值,然後暫停並儲存當前狀態,等下一次 MoveNext() 再繼續。這意味著不管序列有多長,同一時間記憶體裡只存著「當前這一筆」。
上面的流程圖說明了控制流如何在呼叫端與迭代器方法之間切換——藍箭頭是呼叫端推進,橘箭頭是迭代器吐值回來,⏸ 是暫停點,狀態(局部變數、執行位置)在暫停期間由編譯器生成的狀態機保存。
yield 基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public static IEnumerable<int> Fibonacci(int count) { int current = 0; int next = 1;
for (int i = 0; i < count; i++) { yield return current; int temp = next; next = current + next; current = temp; } }
foreach (int number in Fibonacci(10)) { Console.WriteLine(number); }
|
yield return current 執行時,方法暫停在那一行,current 的值傳回給呼叫端;下一次 MoveNext() 觸發,方法從 int temp = next; 繼續往下跑。整個 Fibonacci 序列從頭到尾只用固定幾個 int 的記憶體,不論 count 多大。
yield break 的使用
需要提前結束迭代時,用 yield break:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static IEnumerable<string> ReadUntilEmptyLine(string filePath) { using (StreamReader reader = new StreamReader(filePath)) { string line; while ((line = reader.ReadLine()) != null) { if (string.IsNullOrEmpty(line)) { yield break; } yield return line; } } }
|
using 搭配 yield 是完全合法的——try-finally(無 catch)是允許的,所以資源清理可以放心交給 using。
實際應用
大型檔案逐行處理:上面的 ReadUntilEmptyLine 就是典型場景。不管檔案幾 GB,記憶體裡永遠只有當前那一行。
分批次資料庫查詢:當資料量大到不能一次撈回時,可以用 yield 把分批查詢包成一個連續的序列給呼叫端用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public static IEnumerable<User> GetUsersByPage(DbContext dbContext, int pageSize) { int offset = 0; while (true) { var users = dbContext.Users .Skip(offset) .Take(pageSize) .ToList();
if (!users.Any()) { yield break; }
foreach (var user in users) { yield return user; }
offset += pageSize; } }
|
注意這裡是分批次載入,不是 per-item lazy:每頁的 pageSize 筆資料在 .ToList() 時一次從資料庫撈回,然後逐筆 yield return。呼叫端看到的是連續的序列,但底層是一批一批的 SQL。這與「每次只取一筆」不同,設計時要清楚區分。
無限序列:因為 yield 是按需產生,不需要預先知道邊界,費波那契那個例子改掉 count 上限就是無限序列。
自訂集合類別:實作 IEnumerable<T> 時,yield 省去手寫 IEnumerator 狀態機的樣板程式碼。
最佳實踐與注意事項
try-catch 的限制:try 區塊若帶有 catch 子句,其中不能放 yield return(CS1626);catch 和 finally 區塊同樣不能 yield。try-finally(無 catch)是合法的,using 陳述式就是這樣運作的。
lambda 和匿名方法不能用 yield:yield return 不能放在 lambda 或匿名方法裡(CS1621)。Local function(C# 7+)則可以。如果需要在 lambda 裡產生序列,考慮改成 local function 或拆成獨立方法。
回傳型別限制:yield 方法的回傳型別必須是 IEnumerable<T>、IEnumerable、IEnumerator<T>、IEnumerator,或 C# 8 加入的 IAsyncEnumerable<T>(非同步迭代器,搭配 await foreach 使用)。
避免在 yield return 中執行耗時操作:呼叫端不知道取一個值要花多久,耗時操作應設計成可預期、可取消的形式。
標點統一:中文語境的逗號用全形「,」;程式碼裡用半形。