首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> C# 入門

C# 迭代器深度解析:IEnumerable、yield 關鍵字與兩種實作模式完整指南

     編輯:C# 入門

核心概念:什麼是迭代器?

迭代器 是一種能逐一傳回集合元素的機制,讓開發者使用 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 關鍵字:編譯器的語法糖

語法用途限制
yield return expression;逐一產生元素,expression 必須可隱式轉換為迭代器的元素型別不可在 catch 區塊或含 catch 的 try 區塊中使用
yield break;提前終止迭代可在 finally 區塊使用

核心限制:yield 陳述式只能出現在迭代器區塊(回傳 IEnumerable/IEnumerator 及其泛型版本的方法、get 存取子、運算子)中,不可用於匿名方法、Lambda 運算式、非同步方法、unsafe 區塊。

實作模式一:實作 IEnumerable.GetEnumerator()(單一迭代器)

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,直接回傳 IEnumerable
  • 呼叫端:repo.GetByDepartment("IT") 即可 foreach
  • 更符合單一職責原則:資料存取邏輯封裝在 Repository 內

進階主題:迭代器的執行機制與效能

編譯器生成的狀態機

當您撰寫 yield return,編譯器會產生一個實作 IEnumerator 的巢狀類別,內含:

  • 狀態欄位:追蹤執行進度(-2=未開始, -1=結束, 0=執行中, >0=具體位置)
  • Current 欄位:儲存當前產生的元素
  • 參數/區域變數提升為欄位:讓 MoveNext 能跨呼叫保持狀態
  • Dispose 實作:確保 finally 區塊執行(如釋放資源)

延遲執行特性

// 此時不會執行迴圈內任何程式碼!
var query = numbers.Where(n => n > 5).Select(n => n * 2);

// 只有呼叫 GetEnumerator() 並開始 MoveNext() 時,才會逐一執行
foreach (var item in query) { /* 這裡才真正運算 */ }

優點:記憶體效率高、支援無限序列、可組合查詢。
缺點:每次 foreach 都會重新執行;若需重複使用結果,請先 .ToList() 或 .ToArray() 具體化。

效能比較:yield vs 手建 List

指標yield returnList.Add 後回傳
記憶體O(1) 常數空間O(N) 正比元素數
首元素延遲極低 (即時產生)高 (需建完整 List)
適合場景大量/無限序列、管線處理小量資料、需隨機存取、多次遍歷

常見錯誤與最佳實務

錯誤:在 try-catch 中使用 yield return
// ❌ 編譯錯誤: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;
最佳實務:實作 IDisposable 清理資源
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」的思維。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved