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 走嗎? |
|---|---|---|
陣列 () |
✅ | ✅ |
|
✅ | ✅ |
|
✅(可以列舉 pair、key、value) | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
連字串(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);
}
GO TO FULL VERSION