CodeGym /課程 /C# SELF /Generics 集合的優點

Generics 集合的優點

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

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)。你的集合超級安全,編譯器就像門口的三頭犬一樣守著。

底層怎麼搞的

  • 對參考型別(像 stringobject)來說,不用再多餘的型別轉換。
  • 對值型別(intdouble)來說,"裝箱/拆箱"(boxing/unboxing)的問題直接消失。
  • 在 .NET 用 generics 不會讓程式碼暴增——CLR 會在 JIT 編譯時優化掉。

4. Generics 怎麼讓效能變更好

再強調一次,Generics 給我們的 code 帶來的魔法特性:

型別安全(Type Safety):

這大概是最重要的。因為有 Generics,編譯器就像你的保鑣,不會讓「外人」混進你的集合。你可以超放心 List<Product> 只會有 Product 物件,不會有奇怪的字串或數字。這直接消滅一大堆以前只有執行時才會爆的錯誤(像 InvalidCastException)。你的 code 變得更穩、更可預期。

效能(Performance):

就像剛剛說的,對值型別(像 intdoubleDateTime),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)。集合也是一樣:不用 IntListStringListProductList,只要 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 這種東西,寫太「寬」的泛型,結果編譯時就出現奇怪的錯誤。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION