本文整理自 2003 年 Bruce Eckel 與 Bill Venners 對 C# 之父 Anders Hejlsberg 的深度訪談(第七部分)。訪談圍繞泛型的本質、C#、Java、C++ 三種語言的實現差異、C# 的約束條件以及強/弱類型化的權衡展開,揭示了語言設計背後的工程考量。
Anders Hejlsberg 指出:泛型本質是「參數化類型」或「參數多態」。經典痛點在於陣列與集合的選擇困境:
Customer[]),但缺乏動態增長與豐富操作方法。ArrayList):擁有便利方法,但元素類型為 Object,喪失編譯期類型檢查,且值類型須裝箱/拆箱,效能受損。泛型 List<T> 解決了這一切:編譯期強類型檢查、避免裝箱、保留集合便利性。這就是「有蛋糕也吃蛋糕」。
C# 泛型在 CLR 層面為一等公民。編譯時,泛型類型(如 List<T>)生成包含類型參數元數據的 IL。首次引用特定封閉類型(如 List<int>)時,JIT 編譯器根據 IL 與類型參數生成原生碼。 這意味著:
List<Cat> 就不生成其代碼。List<int>、List<long> 各自擁有專屬原生碼,元素直接以值型存儲,零裝箱。List<Elephant> 與 List<Orangutan> 共用同一組方法代碼(因皆為指標大小),但擁有各自的虛函數表。C# 保留完整泛型類型資訊於運行時。可透過反射獲取 List<T> 的 Type,再結合 typeof(int) 動態構造 List<int> 的 Type 並創建實例。編譯期能做的事,運行時皆可做。
| 維度 | C# 泛型 | Java 泛型(類型擦除) |
|---|---|---|
| 實現層級 | CLR 原生支援(JIT 具現化) | 編譯器層面,JVM 無感知 |
| 運行時類型資訊 | 完整保留 | 擦除為 Object / 原始類型,反射不可知 |
| 值類型效能 | 零裝箱,專用原生碼 | 全數裝箱,極大效能損耗 |
| 動態代碼生成 | 完美支援 | 因類型資訊缺失而受限 |
| 向後相容性 | 需 CLR 2.0+ | 可在舊版 JVM 運行 |
Anders 直言:Java 為兼容舊版 JVM 犧牲了執行效能與反射能力,僅得「語法糖」。
「C# 泛型像是帶類型參數的類;C++ 模板像是長得像類的巨集。」
| 特性 | C# 泛型 | C++ 模板 |
|---|---|---|
| 具現化時機 | 運行時(JIT) | 編譯/鏈接時 |
| 類型檢查 | 定義期強檢查(僅允許 Object 成員) | 具現化期檢查(支援任意運算,失敗報謎之錯誤) |
| 類型系統 | 強類型化 | 弱類型化(結構性鴨子類型) |
| 代碼膨脹 | 引用類型共享代碼,值類型專用 | 每種具現化皆生成獨立代碼 |
| 分發編譯 | 支援(預編譯 IL) | 難(需原始碼可見) |
C# 選擇定義期強檢查,保證泛型代碼對任意合法類型參數皆安全;C++ 採用結構性檢查,靈活但錯誤訊息難懂,且易導致代碼膨脹。
class List<T> where T : IComparable { ... }
class Dictionary<K, V> where K : IComparable { ... }
where 子句可指定:基類、介面(多個)、無參建構函式(new())、值/引用類型(struct/class)。
key.CompareTo(...),若 K 未實作 IComparable,運行時拋異常。C# 約束不支援運算符(如 +)、靜態成員、特定簽名方法。Anders 解釋:運算符為靜態成員,無法納入介面約束;全模式匹配過度複雜,投資報酬比低。
替代模式:策略/工廠注入
class Matrix<T> {
public T DotProduct(T a, T b, ICalculator<T> calc)
=> calc.Multiply(a, b);
}
interface ICalculator<T> { T Multiply(T a, T b); }
透過注入具體計算器,繞過泛型約束限制,保持類型安全。
Bruce Eckel 比喻類型化為「頻譜」:C++ 模板偏弱,加約束向強邁進。Anders 補充:這是一個「撥盤」——
C# 定位於「實用強類型」:預設安全,必要時透過約束、工廠模式、動態類型(dynamic)等釋放靈活度。