本节摘要:C# 泛型由 CLR 在运行时"具化"——每个值类型参数得到独立机器码,引用类型共享一份。本节对比 Java 的类型擦除讲清这套机制,说明泛型约束如何用编译期检查换取安全性,以及协变逆变这些"型变规则"到底在保护什么。
第一章的装箱实验已经给出一半答案:ArrayList 装 int,每次 Add 都装箱。另一半答案是类型安全:
var list = new ArrayList { 1, 2, 3 }; string s = (string)list[0]; // 编译通过,运行时 InvalidCastException
编译器无法知道列表里是什么,强转错误要等运行时爆炸。泛型一举解决两个问题:
var list = new List<int> { 1, 2, 3 }; string s = (string)list[0]; // CS0030:编译期直接拒绝
List<int> 的元素是连续的 int 数组,没有装箱,缓存友好;类型检查在编译期完成。一个语言特性同时买回性能与安全,这在语言设计史上并不多见。
泛型怎么落地到运行时?C# 与 Java 给出了相反的答案。Java 选择擦除:编译后类型参数被抹掉,List<Integer> 与 List<String> 运行时是同一个类,等于自动装箱的集合加编译期语法检查。CLR 选择具化:类型参数保留到运行时,JIT 为每个不同的值类型参数生成一份专属机器码;所有引用类型参数共享一份(因为引用都是同样大小的指针,只需指针替换)。

具化的直接红利你已经见过:List<int> 零装箱。还有一条常被忽略的:运行时能拿到类型参数——typeof(List<int>) 合法,反射、序列化框架可以区分元素类型,泛型数学(INumber<T> 接口让一套算法跑在所有数值类型上)也依赖具化。代价是代码膨胀:用一百种值类型实例化同一个泛型类,JIT 最多生成一百份机器码。对绝大多数应用这点内存无感,但它是真实存在的账。
public static T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;
没有约束时,T 是"任意类型",编译器只许你做 object 能做的事。where T : IComparable<T> 是一份声明:调用者必须传入可比较的类型。换来的是方法体内能直接调 CompareTo——编译器据此放行。常用约束从弱到强:struct(值类型)、class(引用类型)、接口约束、基类约束、new()(有无参构造,可 new T())。原则很简单:用到什么能力就约束什么,约束越少泛型越通用。约束是写给编译器的证据:你声明得越准确,它能替你拦下的错误越多。
⚠️ 常见坑:想对 T 做算术,新手写
a + b直接编译报错——运算符是静态成员,接口约束默认不含运算符。历史解法是动态分派或表达式树编译,.NET 7 的INumber<T>泛型数学是官方正解。
IEnumerable<object> objs = new List<string>(); // OK:协变 // IList<object> list = new List<string>(); // CS0266:不变
IEnumerable<out T> 的 out 标记协变:只读地消费 T,IEnumerable<string> 可以安全当 IEnumerable<object> 用——读出来的是 string,天然也是 object。IList<T> 可读可写,若允许这样转换,下一步 list.Add(new Cat()) 就把猫塞进了字符串列表,类型系统当场穿仓。Action<in T> 相反是逆变:只写不读,接受更宽参数类型的委托可以当更窄的 delegate 用。型变规则不是语法装饰,是编译器在阻止"经由泛型偷运不兼容对象"的通路。设计自己的泛型接口时,能标 out/in 就标——它决定了使用者的组合自由度。
把第一章的 ArrayList 实验补上对照:百万次求和,ArrayList(装箱加拆箱)与 List(连续 int 数组)的时间差常在十倍以上,GC 分配差三个数量级。这不是"泛型更快"的魔法,而是"具化让值类型保持值类型"的自然结果。记住因果链:具化 → 无装箱 → 连续内存 → 缓存命中高,GC 压力小。第四章讲 LOH 与数组池化时,这条链会再延伸两环。
把视角拉高一层收束:泛型具化、装箱、表达式树这三件事在运行时层面是同一主题的三个侧面——类型信息保留得越多,运行时能做的优化与检查就越多,付出的空间与代码生成就越大。C# 在这条轴上选择了偏运行时的一端,于是有了零装箱的集合、能辨类型的反射与泛型数学;Java 选择了偏编译期的一端,换来更薄的运行时。理解这个权衡比记住任何一方的优劣更有价值:将来你评估任何语言的类型系统设计,都可以问同一句话——类型信息在运行时还剩多少,剩下的那份换来了什么。
下一节看泛型的主战场:各类集合的底层数据结构与选型决策。