CodeGym /課程 /C# SELF /可列舉契約: IEnumerable<T>...

可列舉契約: IEnumerable<T>

C# SELF
等級 28, 課堂 1
開放

1. 前言

你可能已經注意到,C# 裡不同的集合有一些共通點。舉例來說,幾乎所有集合我們都可以用 foreach 迴圈來走過一遍:


var names = new List<string> { "安雅", "鮑里斯", "維卡" };

// 注意 — 這段程式碼不只 List,陣列、甚至 HashSet 也能用!
foreach (var name in names)
{
    Console.WriteLine(name);
}

魔法在哪?其實就是所有現代集合都實作了 IEnumerable<T> 這個介面 —— 一種「契約」,保證集合能一個一個、依序把元素吐出來。

想像一下公司裡,任何一組員工(不管是部門、專案團隊、還是尾牙參加名單)都有個規則:只要物件有實作 "IEnumerable" 介面,你就一定可以照順序把所有員工走一遍,不用管「怎麼存的」,重點是「能不能一個個列舉」。

2. IEnumerable<T> 介面裡有什麼?

來看看 .NET 裡這個介面長怎樣:


public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator();
}

它 只有一個方法 —— GetEnumerator,回傳一個 IEnumerator<T> 物件。這個物件負責「列舉」的過程:它知道現在在哪個元素、怎麼走到下一個、什麼時候結束。

簡單說: 只要一個 class(或集合)實作 IEnumerable<T>,你就可以用 foreach 走過它,依序拿到每個元素 —— 完全不用管裡面怎麼實作的:可以是 list、hash table、甚至硬碟上的資料都行。

小圖解


集合(例如 List<T>)
    |
    v
IEnumerable<T>
    |
    v
IEnumerator<T>(真正「跑」元素的傢伙)

3. 實戰與程式碼通用性

這種契約最大的好處就是 通用性。如果一個 function 或 method 接收 IEnumerable<T>,代表它可以吃任何集合型別 —— 從陣列、list 到 hash set、queue,甚至你自己寫的 collection 都行!

舉例,來看一個計算元素總和的 function:


// 只要支援 IEnumerable<int>,什麼 int 集合都能算總和
int Sum(IEnumerable<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
        result += n;
    return result;
}

// List<int> 也能用
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(Sum(list));

// 陣列也沒問題!
int[] array = { 4, 5, 6 };
Console.WriteLine(Sum(array));

// 甚至可以直接用 LINQ 過濾後的結果
Console.WriteLine(Sum(list.Where(x => x % 2 == 0))); // 用 LINQ

生活例子:通用方法

假設你要寫一個工具,找出任何字串集合裡最長的單字。因為有 IEnumerable<string>,你可以對任何來源做這件事 —— 陣列、list、過濾後的結果都行:


string FindLongest(IEnumerable<string> words)
{
    string longest = "";
    foreach (var word in words)
        if (word.Length > longest.Length)
            longest = word;
    return longest;
}

用什麼集合都可以。

4. 為什麼 foreach 迴圈能用在 IEnumerable<T>?

常見問題:為什麼 foreach 迴圈「懂」各種集合?答案很簡單:C# 編譯器會找 class 裡有沒有 GetEnumerator 方法,然後預期會拿到一個有 MoveNext() 方法和 Current 屬性的物件。這就是標準介面 —— IEnumerator<T>。

這樣一來,你甚至可以自己寫一個 class,讓它「一個一個」產生元素(像是費波那契數列產生器),只要它實作 IEnumerable<int>,就能用 foreach 跟 list 一樣用。

5. 什麼是 Enumerator?它怎麼運作?

任何實作 IEnumerable<T> 的集合裡面,都藏著一個特別的「列舉器」—— Enumerator(或學名:實作 IEnumerator<T> 介面的物件)。就是它在你寫 foreach 迴圈時,一個一個把元素「搬」出來。

IEnumerator<T> 介面

標準 Enumerator 能做這些:


public interface IEnumerator<T> : IDisposable
{
    T Current { get; }         // 現在的元素
    bool MoveNext();           // 移到下一個元素
    void Reset();              // 回到開頭(很少用)
}

裡面怎麼運作?

  • MoveNext() —— 把「指標」移到下一個元素,如果還有元素就回傳 true。沒了就 false。
  • Current —— 拿到現在指到的元素。
  • Reset() —— 把 Enumerator 重設回開頭(但幾乎沒人在用)。
  • Dispose() —— 釋放資源(如果集合有用到檔案或網路就會需要)。

範例:foreach 背後怎麼跑的

你寫:


var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
    Console.WriteLine(n);

其實編譯器會變成這樣:


var numbers = new List<int> { 1, 2, 3 };

// 取得「列舉器」
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
    var n = enumerator.Current;
    Console.WriteLine(n);
}
// 如果 enumerator 有實作 IDisposable,編譯器會自動用 using 幫你呼叫 Dispose()

重點: 如果集合有用到外部資源(像檔案、資料庫),Enumerator 會在結束時自動釋放它們。

流程圖


開始走訪 -> GetEnumerator() -> Enumerator
         |
         v
  MoveNext() -> Current
         |
         v
  MoveNext() -> Current
         |
        ...
         |
         v
  MoveNext() == false -> 結束

6. IEnumerable 跟陣列、list、set 的關係

來看看 .NET 標準容器哪些有實作這個介面:

集合型別 有實作 IEnumerable<T>? 可以用 foreach 走嗎?
陣列 (
int[]
)
✅ ✅
List<T>
✅ ✅
Dictionary<TKey, V>
✅(可以列舉 pair、key、value) ✅
HashSet<T>
✅ ✅
Queue<T>
✅ ✅
Stack<T>
✅ ✅

連字串(string)都實作了一般的 IEnumerable,所以你也可以用 foreach 走過每個字元。

7. 自己實作 Enumerable

有趣的挑戰:來寫一個最小的集合,裡面存 0 到 N 的所有偶數,並實作 IEnumerable<int>。這樣我們就能用 foreach 走它,也能配 LINQ。


// 可以「列舉」的集合 class
class EvenNumbers : IEnumerable<int>
{
    private int max;

    public EvenNumbers(int max)
    {
        this.max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i <= max; i += 2)
            yield return i; // 這裡用 yield return 直接幫你做 enumerator
    }

    // 明確實作非泛型 IEnumerable 介面,為了相容性
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

// 用法:
var evens = new EvenNumbers(10);
foreach(var e in evens)
    Console.Write($"{e} "); // 0 2 4 6 8 10

重點:只要你的 class 實作 IEnumerable<T> —— 你就自動跟大部分 .NET 工具相容:LINQ、foreach、還有所有吃 enumerable 的方法。

8. 常見錯誤與細節

有時新手會以為 IEnumerable<T> 是一種「有元素」的集合。其實它只是個「承諾」:如果你開始列舉,我會一個一個給你元素。

如果你要隨機存取(myList[5])、用 Add 或 Remove 這種方法 —— IEnumerable<T> 幫不上忙。它只管「依序走」!

錯誤範例:在列舉時修改集合。像這樣:


foreach (var item in myList)
{
    if (item < 0)
        myList.Remove(item); // 危險!InvalidOperationException
}

比較安全的做法是先另外存要刪的,再處理,或用會產生新集合的方法:


// 安全做法 —— 先手動建立新集合
var newList = new List<int>();
foreach (var item in myList)
{
    if (item >= 0)
        newList.Add(item);
}
myList = newList;

// 或者反向用 index 刪除
for (int i = myList.Count - 1; i >= 0; i--)
{
    if (myList[i] < 0)
        myList.RemoveAt(i);
}
2
任務
C# SELF, 等級 28, 課堂 1
上鎖
實作一個支援 IEnumerable 的自訂集合
實作一個支援 IEnumerable 的自訂集合
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION