迭代器 是一種能逐一傳回集合元素的機制,讓開發者使用 foreach 遍歷自訂資料結構,而無需手動實作完整的 IEnumerable / IEnumerator 介面。C# 2.0 引入 yield 關鍵字,編譯器會自動生成狀態機器,大幅簡化開發複雜度。
// 非泛型版本
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
// 泛型版本 (建議優先使用)
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator : IDisposable
{
T Current { get; }
bool MoveNext();
}
| 語法 | 用途 | 限制 |
|---|---|---|
yield return expression; | 逐一產生元素,expression 必須可隱式轉換為迭代器的元素型別 | 不可在 catch 區塊或含 catch 的 try 區塊中使用 |
yield break; | 提前終止迭代 | 可在 finally 區塊使用 |
核心限制:yield 陳述式只能出現在迭代器區塊(回傳 IEnumerable/IEnumerator 及其泛型版本的方法、get 存取子、運算子)中,不可用於匿名方法、Lambda 運算式、非同步方法、unsafe 區塊。
using System;
using System.Collections;
using System.Collections.Generic;
public class NumberSequence : IEnumerable
{
private readonly int _start;
private readonly int _count;
public NumberSequence(int start, int count)
{
_start = start;
_count = count;
}
// 核心:實作泛型 GetEnumerator,編譯器自動生成狀態機
public IEnumerator GetEnumerator()
{
for (int i = 0; i < _count; i++)
{
yield return _start + i;
}
}
// 非泛型介面顯式實作,轉呼叫泛型版本
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// 使用方式
var seq = new NumberSequence(10, 5); // 10, 11, 12, 13, 14
foreach (var n in seq) Console.WriteLine(n);
GetEnumerator 迭代器邏輯List、Array)foreach、LINQ 擴充方法 (Where, Select 等)public class EmployeeRepository
{
private readonly List _employees = new();
// 迭代器方法 1:依部門篩選
public IEnumerable GetByDepartment(string department)
{
foreach (var emp in _employees)
if (emp.Department == department)
yield return emp;
}
// 迭代器方法 2:薪資前 N 高
public IEnumerable GetTopPaid(int count)
{
var sorted = _employees.OrderByDescending(e => e.Salary).Take(count);
foreach (var emp in sorted)
yield return emp;
}
// 迭代器方法 3:即將生日
public IEnumerable GetUpcomingBirthdays(int daysAhead = 30)
{
var today = DateTime.Today;
var cutoff = today.AddDays(daysAhead);
foreach (var emp in _employees)
{
var nextBirthday = new DateTime(today.Year, emp.BirthDate.Month, emp.BirthDate.Day);
if (nextBirthday < today) nextBirthday = nextBirthday.AddYears(1);
if (nextBirthday <= cutoff)
yield return emp;
}
}
}
IEnumerable,直接回傳 IEnumerablerepo.GetByDepartment("IT") 即可 foreach當您撰寫 yield return,編譯器會產生一個實作 IEnumerator 的巢狀類別,內含:
MoveNext 能跨呼叫保持狀態finally 區塊執行(如釋放資源)// 此時不會執行迴圈內任何程式碼!
var query = numbers.Where(n => n > 5).Select(n => n * 2);
// 只有呼叫 GetEnumerator() 並開始 MoveNext() 時,才會逐一執行
foreach (var item in query) { /* 這裡才真正運算 */ }
優點:記憶體效率高、支援無限序列、可組合查詢。
缺點:每次 foreach 都會重新執行;若需重複使用結果,請先 .ToList() 或 .ToArray() 具體化。
| 指標 | yield return | List.Add 後回傳 |
|---|---|---|
| 記憶體 | O(1) 常數空間 | O(N) 正比元素數 |
| 首元素延遲 | 極低 (即時產生) | 高 (需建完整 List) |
| 適合場景 | 大量/無限序列、管線處理 | 小量資料、需隨機存取、多次遍歷 |
// ❌ 編譯錯誤:yield return 不能在含 catch 的 try 區塊中
public IEnumerable Bad()
{
try { yield return 1; }
catch { yield return 0; }
}
// ✅ 正確:將 try 移至迴圈內,或用 yield break 處理異常
public IEnumerable Good()
{
foreach (var item in source)
{
try { yield return Process(item); }
catch { /* 記錄錯誤 */ yield break; }
}
}
var query = GetDataFromDb().Where(x => x.Active); // 假設 GetDataFromDb 含 yield
var count = query.Count(); // 第一次遍歷:查詢 DB
var list = query.ToList(); // 第二次遍歷:再查一次 DB!
// 解決:具體化
var materialized = query.ToList();
var count = materialized.Count;
var list = materialized;
public IEnumerable ReadLines(string path)
{
using (var reader = File.OpenText(path)) // using 區塊會被編譯器保留在 Dispose 中
{
string line;
while ((line = reader.ReadLine()) != null)
yield return line;
} // foreach 結束或 yield break 時,自動呼叫 Dispose 關閉檔案
}
| 場景 | 推薦模式 | 理由 |
|---|---|---|
| 類別本身代表單一序列 (如自訂集合) | 模式一:實作 IEnumerable | 語意自然,支援標準介面,可直接 foreach 實例 |
| 需提供多種查詢/篩選方式 | 模式二:多個具名迭代器方法 | API 清楚表達意圖,遵循介面隔離原則 |
| 資料來源昂貴 (DB、Web API、大檔案) | 兩者皆可,善用 yield 延遲執行 | 避免一次性載入所有資料,節省記憶體與啟動時間 |
| 需在迭代中管理非受控資源 | 模式一 + using 區塊 | 編譯器確保 Dispose 被呼叫 |
掌握 yield 與迭代器模式,是寫出高效能、低記憶體、可組合 C# 程式碼的關鍵技能。建議從今天開始,將「先建 List 再回傳」的習慣,改為「直接 yield return」的思維。