1. 小小歷史背景
在 C# 的遠古時代,操作集合只有兩種快樂:用陣列,或是用所謂「型別不嚴格」的集合——像是 ArrayList,你可以丟進去任何型別的物件。看起來很自由對吧?但只要你把字串和整數混著丟進去,馬上就亂成一團:你沒辦法安全地取出元素,除非加一堆(還不一定有用的)型別檢查。還有——會莫名其妙出現超怪的錯誤,然後你就要開始跳型別轉換的舞。
非泛型集合的範例(ArrayList):
using System.Collections;
ArrayList stuff = new ArrayList();
stuff.Add(42);
stuff.Add("Hello C#");
int number = (int)stuff[0]; // OK
string text = (string)stuff[1]; // OK
int fail = (int)stuff[1]; // 砰!InvalidCastException(執行時期錯誤)
沒錯,編譯器完全不會罵你——錯誤只會在執行時才爆出來!這就像打開冰箱,結果裡面有個熱鍋——驚喜!
泛型的精髓是什麼?
泛型集合(Generics)就是為了讓你能建立「保證型別」的集合。這帶來三個超重要的好處:
- 編譯時期型別安全。 編譯器不會讓你亂塞東西進集合。
- 方便: 不用每次都手動轉型。
- 效能: 不會浪費時間在沒必要的 "裝箱與拆箱"(boxing/unboxing)。
2. 到底什麼是 Generics(泛型)?
Generics 就是一種魔法,讓你可以描述資料結構和方法,可以處理任何型別,但又超嚴格型別檢查。
想像有個萬用箱子,可以裝書也可以裝襪子,但一次只能裝一種。如果箱子標明「只能裝書」——誰都不能偷偷塞襪子進去。泛型集合也是這樣:你建立 int 的集合,就不可能混進 string。
泛型類別宣告範例
public class Box<T>
{
public T Value { get; set; }
}
var boxOfInt = new Box<int> { Value = 42 };
var boxOfString = new Box<string> { Value = "Hello Generics!" };
T 就是「型別參數」,你要明確告訴 C# 你的箱子要裝什麼。C# 會強制你每次都要指定型別。
.NET 裡的泛型集合
在 .NET Framework 幾乎所有現代集合都有泛型版。像是:
- List<T> —— 動態元素清單,型別是 T。
- Dictionary<TKey, TValue> —— 關聯陣列(字典),有 key 和 value。
- Queue<T>、Stack<T> —— 佇列和堆疊。
- 還有很多其他的。
3. 怎麼運作:Generics 的原理
型別參數
當你宣告像 List<int> 這種集合時,C# 編譯器會針對 int 產生一個專屬版本(specialization)。你宣告 List<string>,編譯器又會產生一個專門給字串的版本,依此類推。
對你這個寫 code 的來說,這根本就是魔法:
List<int> numbers = new List<int>();
numbers.Add(1); // 只能加 int
List<string> words = new List<string>();
words.Add("hello"); // 只能加 string
如果你硬要加錯型別,編譯器馬上就不爽:
numbers.Add("fail"); // 編譯時期就錯!
型別安全:Compile-time vs. Run-time
這代表像上面那種錯誤,根本不會等到程式跑起來才爆(run-time)。你的集合超級安全,編譯器就像門口的三頭犬一樣守著。
底層怎麼搞的
- 對參考型別(像 string、object)來說,不用再多餘的型別轉換。
- 對值型別(int、double)來說,"裝箱/拆箱"(boxing/unboxing)的問題直接消失。
- 在 .NET 用 generics 不會讓程式碼暴增——CLR 會在 JIT 編譯時優化掉。
4. Generics 怎麼讓效能變更好
再強調一次,Generics 給我們的 code 帶來的魔法特性:
型別安全(Type Safety):
這大概是最重要的。因為有 Generics,編譯器就像你的保鑣,不會讓「外人」混進你的集合。你可以超放心 List<Product> 只會有 Product 物件,不會有奇怪的字串或數字。這直接消滅一大堆以前只有執行時才會爆的錯誤(像 InvalidCastException)。你的 code 變得更穩、更可預期。
效能(Performance):
就像剛剛說的,對值型別(像 int、double 或 DateTime),Generics 讓你用記憶體和 CPU 都更有效率。對那種要存幾百萬筆資料或常常要改動的集合,這超級重要。就像你不用一直把橘子從箱子倒到袋子再倒回來,你直接放進正確的箱子就好。
程式碼重用(Code Reusability):
Generics 讓你寫一次 code,可以處理各種型別,不用一直複製貼上。比如你要寫個交換兩個變數的函式。沒 Generics 你要寫 SwapInt(ref int a, ref int b)、SwapString(ref string a, ref string b)、SwapProduct(ref Product a, ref Product b) 之類。用 Generics 你只要一個 Swap<T>(ref T a, ref T b)。集合也是一樣:不用 IntList、StringList、ProductList,只要 List<T>。這讓你的 code 更精簡、好維護、好擴充。
5. 泛型方法和自訂 generic 類別
泛型方法
Generics 不只用在集合!你可以寫自己的泛型方法,處理任何型別,讓 code 更萬用。
//型別參數 T 寫在方法名後面
public static void Swap<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
// 用法:
int a = 10, b = 20;
Swap(ref a, ref b); // 現在 a == 20, b == 10
string one = "一", two = "二";
Swap(ref one, ref two); // 字串也可以!
注意,C# 編譯器可以根據你傳進去的變數自動判斷型別參數。所以上面例子裡呼叫 Swap 不用再特別指定型別。
自訂 generic 類別
你甚至可以自己做「箱子」(generic 類別)。其實超簡單:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
// 用法:
var pair = new Pair<int, string> { First = 42, Second = "答案" };
6. 泛型集合的使用範例
回到我們之前講過的「待辦清單」(To-Do List)例子。以前我們用陣列存任務,或直接印出來。現在——我們用 List<string> 動態存。
範例:動態新增任務到清單
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> tasks = new List<string>();
Console.WriteLine("請輸入任務(或直接按 Enter 結束):");
string input;
while (!string.IsNullOrWhiteSpace(input = Console.ReadLine()))
{
tasks.Add(input);
Console.WriteLine("任務已加入!再輸入一個(或直接按 Enter 結束):");
}
Console.WriteLine("\n你今天的待辦清單:");
foreach (string task in tasks)
{
Console.WriteLine("- " + task);
}
}
}
這裡有什麼好處?
- 集合會自動隨著你加任務而變大。
- 不可能不小心加進去非字串的東西。
- 可以很輕鬆地遍歷清單、印出內容。
7. 新手常見錯誤、特點和建議
從非泛型集合轉到泛型,有時會讓新手有點混亂。這裡有幾個常見狀況:
- 想加錯型別的元素(List<int> numbers = new List<int>(); numbers.Add("hi"); // 錯誤)。
- 想在同一個集合混用不同型別——這時你要選 List<object> 這種基底型別——但之後每次都要自己轉回正確型別。
- 忘記有 constraints 這種東西,寫太「寬」的泛型,結果編譯時就出現奇怪的錯誤。
GO TO FULL VERSION