C# yield 執行流程(caller ↔ iterator)
<!-- Caller 欄 -->
<rect x="30" y="60" width="180" height="360" rx="8" fill="none" stroke="#9E9E9E" stroke-width="1.5" stroke-dasharray="4,3"/>
<text x="120" y="82" text-anchor="middle" font-size="13" fill="#555">呼叫端(foreach)</text>

<!-- Iterator 欄 -->
<rect x="350" y="60" width="180" height="360" rx="8" fill="none" stroke="#9E9E9E" stroke-width="1.5" stroke-dasharray="4,3"/>
<text x="440" y="82" text-anchor="middle" font-size="13" fill="#555">迭代器方法</text>

<!-- Caller 步驟 -->
<rect x="50" y="100" width="140" height="36" rx="5" fill="#90CAF9"/>
<text x="120" y="123" text-anchor="middle" font-size="13">MoveNext()</text>

<rect x="50" y="220" width="140" height="36" rx="5" fill="#90CAF9"/>
<text x="120" y="243" text-anchor="middle" font-size="13">讀取 Current</text>

<rect x="50" y="300" width="140" height="36" rx="5" fill="#90CAF9"/>
<text x="120" y="323" text-anchor="middle" font-size="13">MoveNext() 再次</text>

<rect x="50" y="390" width="140" height="30" rx="5" fill="#EF9A9A"/>
<text x="120" y="410" text-anchor="middle" font-size="12">MoveNext()→false</text>

<!-- Iterator 步驟 -->
<rect x="370" y="160" width="140" height="36" rx="5" fill="#81C784"/>
<text x="440" y="178" text-anchor="middle" font-size="12" fill="#1B5E20">▶ 執行到</text>
<text x="440" y="192" text-anchor="middle" font-size="12" fill="#1B5E20">yield return 值</text>

<rect x="370" y="210" width="140" height="36" rx="5" fill="#FFB74D"/>
<text x="440" y="230" text-anchor="middle" font-size="12">⏸ 暫停、儲存狀態</text>
<text x="440" y="244" text-anchor="middle" font-size="11" fill="#555">(局部變數、位置)</text>

<rect x="370" y="320" width="140" height="36" rx="5" fill="#81C784"/>
<text x="440" y="340" text-anchor="middle" font-size="12" fill="#1B5E20">▶ 從暫停點繼續</text>
<text x="440" y="354" text-anchor="middle" font-size="12" fill="#1B5E20">執行</text>

<!-- 箭頭:MoveNext → 迭代器開始執行 -->
<path d="M190 118 L370 178" stroke="#2196F3" stroke-width="1.8" fill="none" marker-end="url(#arr)"/>
<!-- 箭頭:暫停後回傳值給 caller -->
<path d="M370 228 L190 238" stroke="#FF9800" stroke-width="1.8" fill="none" marker-end="url(#arr2)"/>
<!-- 箭頭:再次 MoveNext → 迭代器繼續 -->
<path d="M190 318 L370 338" stroke="#2196F3" stroke-width="1.8" fill="none" marker-end="url(#arr)"/>
<!-- 箭頭:yield break / 方法結束 → false -->
<path d="M370 390 L190 400" stroke="#F44336" stroke-width="1.8" fill="none" marker-end="url(#arr2)"/>

<!-- 箭頭定義 -->
<defs>
    <marker id="arr" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
        <path d="M0,0 L8,4 L0,8" fill="none" stroke="#2196F3" stroke-width="1.5"/>
    </marker>
    <marker id="arr2" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
        <path d="M0,0 L8,4 L0,8" fill="none" stroke="#FF9800" stroke-width="1.5"/>
    </marker>
</defs>

為什麼需要 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
// 使用 yield return 產生費波那契數列
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
// 假設 dbContext 已透過 DI 注入
public static IEnumerable<User> GetUsersByPage(DbContext dbContext, int pageSize)
{
int offset = 0;
while (true)
{
// 每次查一批,.ToList() 在這裡執行實際 SQL
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);catchfinally 區塊同樣不能 yieldtry-finally(無 catch)是合法的,using 陳述式就是這樣運作的。

lambda 和匿名方法不能用 yieldyield return 不能放在 lambda 或匿名方法裡(CS1621)。Local function(C# 7+)則可以。如果需要在 lambda 裡產生序列,考慮改成 local function 或拆成獨立方法。

回傳型別限制yield 方法的回傳型別必須是 IEnumerable<T>IEnumerableIEnumerator<T>IEnumerator,或 C# 8 加入的 IAsyncEnumerable<T>(非同步迭代器,搭配 await foreach 使用)。

避免在 yield return 中執行耗時操作:呼叫端不知道取一個值要花多久,耗時操作應設計成可預期、可取消的形式。

標點統一:中文語境的逗號用全形「,」;程式碼裡用半形。