第 11 章:泛型的底层实现

jerry北京市2026年4月21日C# 5 次阅读 约 17 分钟
第 11 章:泛型的底层实现

深入理解 CLR 泛型与 Java 类型擦除的区别、值类型和引用类型的类型特化、协变与逆变、泛型约束的本质,以及泛型中的静态字段陷阱。


11.1 CLR 泛型 vs Java 类型擦除

Java 的类型擦除

Java 泛型在编译后会擦除类型信息:

// Java 编译前
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);

// Java 编译后(类型擦除)
List list = new ArrayList();       // 泛型信息消失
list.add("hello");
String s = (String) list.get(0);   // 插入强制转换

CLR 的真泛型(Reification)

.NET 泛型在运行时保留完整的类型信息:

var list = new List<int>();
Console.WriteLine(list.GetType().Name);                    // List`1
Console.WriteLine(list.GetType().GetGenericArguments()[0]); // System.Int32

// 运行时可以区分不同的泛型实例
Console.WriteLine(typeof(List<int>) == typeof(List<string>)); // False

// 可以在运行时构造泛型类型
Type openType = typeof(List<>);
Type closedType = openType.MakeGenericType(typeof(double));
object instance = Activator.CreateInstance(closedType)!;
// instance 是 List<double>
特性 CLR 泛型 Java 泛型
运行时类型信息 保留 擦除
值类型支持 原生支持(无装箱) 不支持(必须用包装类)
运行时反射 完整泛型信息 只能获取原始类型
代码生成 JIT 为不同类型生成专用代码 共享同一份字节码

11.2 类型特化(Type Specialization)

CLR 对值类型和引用类型采用不同的策略:

值类型:每种类型生成独立的 JIT 代码

// 这三个会生成三份不同的机器码
List<int> intList = new();       // 专用的 int 版本
List<double> doubleList = new(); // 专用的 double 版本
List<bool> boolList = new();     // 专用的 bool 版本
JIT 编译结果:
┌─────────────────────┐
│ List<int> 的机器码    │  ← 直接操作 4 字节整数,无装箱
├─────────────────────┤
│ List<double> 的机器码 │  ← 直接操作 8 字节浮点数
├─────────────────────┤
│ List<bool> 的机器码   │  ← 直接操作 1 字节布尔值
└─────────────────────┘

为什么值类型需要独立代码?

  • 不同值类型大小不同(int 4字节,double 8字节)
  • 内存布局不同,需要不同的 CPU 指令
  • 避免装箱,直接操作原始值

引用类型:共享同一份 JIT 代码

// 这三个共享同一份机器码
List<string> stringList = new();
List<User> userList = new();
List<object> objectList = new();
JIT 编译结果:
┌──────────────────────────┐
│ List<引用类型> 的共享机器码 │  ← 所有引用类型共用
│ (操作的都是指针,大小相同) │
└──────────────────────────┘

为什么引用类型可以共享?

  • 所有引用类型的变量都是指针(8字节,64位系统)
  • 内存布局相同,可以用相同的 CPU 指令操作

11.3 协变(Covariance)与逆变(Contravariance)

协变(out):子类型 → 父类型

// IEnumerable<T> 声明为 IEnumerable<out T>
IEnumerable<string> strings = new List<string> { "a", "b" };
IEnumerable<object> objects = strings;  // 协变:string → object ✅

// 协变只允许 T 出现在输出位置
interface IProducer<out T>
{
    T Produce();           // ✅ T 在返回值(输出位置)
    // void Consume(T item); // ❌ 编译错误:T 不能在参数(输入位置)
}

逆变(in):父类型 → 子类型

// Action<T> 声明为 Action<in T>
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction;  // 逆变:object → string ✅
stringAction("hello"); // 可以工作,因为 string 是 object

// 逆变只允许 T 出现在输入位置
interface IConsumer<in T>
{
    void Consume(T item);  // ✅ T 在参数(输入位置)
    // T Produce();         // ❌ 编译错误:T 不能在返回值(输出位置)
}

为什么需要协变和逆变?

// 没有协变,这段代码无法编译
void PrintAll(IEnumerable<object> items)
{
    foreach (var item in items)
        Console.WriteLine(item);
}

List<string> names = ["张三", "李四"];
PrintAll(names);  // 因为 IEnumerable<out T> 是协变的,所以可以

// 没有逆变,这段代码无法编译
void SortUsers(IComparer<User> comparer) { /* ... */ }

class EntityComparer : IComparer<object>
{
    public int Compare(object? x, object? y) => /* ... */;
}

SortUsers(new EntityComparer()); // 因为 IComparer<in T> 是逆变的,所以可以

常见的协变/逆变接口

接口 变体 说明
IEnumerable<out T> 协变 只读序列
IReadOnlyList<out T> 协变 只读列表
IComparable<in T> 逆变 比较
IComparer<in T> 逆变 比较器
Action<in T> 逆变 无返回值委托
Func<out TResult> 协变 有返回值委托
Func<in T, out TResult> 混合 输入逆变,输出协变

11.4 泛型约束

约束类型

// 值类型约束
void Process<T>(T value) where T : struct { }

// 引用类型约束
void Process<T>(T value) where T : class { }

// 非空约束(C# 9+)
void Process<T>(T value) where T : notnull { }

// 构造函数约束
T Create<T>() where T : new() => new T();

// 基类约束
void Save<T>(T entity) where T : BaseEntity { }

// 接口约束
void Sort<T>(List<T> list) where T : IComparable<T> { }

// 多重约束
void Process<T>(T value) where T : class, IDisposable, new() { }

// 类型参数约束
void Copy<TSource, TDest>(TSource src) where TDest : TSource { }

// .NET 7+:静态抽象成员约束
T Add<T>(T a, T b) where T : INumber<T> => a + b;

约束的底层作用

// 没有约束:T 被当作 object,可能需要装箱
void NoConstraint<T>(T a, T b)
{
    // a == b;  // 编译错误:不能用 == 比较
    a.Equals(b); // 如果 T 是值类型,会装箱
}

// 有约束:编译器知道 T 的能力,可以生成更高效的代码
void WithConstraint<T>(T a, T b) where T : IEquatable<T>
{
    a.Equals(b); // 调用 IEquatable<T>.Equals,值类型不装箱
}

11.5 泛型中的静态字段

每个封闭泛型类型有自己独立的静态字段:

class Counter<T>
{
    public static int Count = 0;
}

Counter<int>.Count = 10;
Counter<string>.Count = 20;

Console.WriteLine(Counter<int>.Count);    // 10
Console.WriteLine(Counter<string>.Count); // 20
Console.WriteLine(Counter<double>.Count); // 0(独立的)
内存中的静态字段:
┌──────────────────┐
│ Counter<int>     │ → Count = 10
├──────────────────┤
│ Counter<string>  │ → Count = 20
├──────────────────┤
│ Counter<double>  │ → Count = 0
└──────────────────┘
每个封闭类型都是独立的"类",有自己的静态字段

利用这个特性实现类型缓存

// 高效的类型信息缓存(比 Dictionary<Type, T> 更快)
static class TypeCache<T>
{
    // 每个 T 只计算一次,之后直接读取静态字段
    public static readonly int Size = Unsafe.SizeOf<T>();
    public static readonly bool IsValueType = typeof(T).IsValueType;
    public static readonly string TypeName = typeof(T).FullName ?? typeof(T).Name;
}

Console.WriteLine(TypeCache<int>.Size);        // 4
Console.WriteLine(TypeCache<int>.IsValueType); // True
Console.WriteLine(TypeCache<long>.Size);       // 8

11.6 泛型的限制

// 不能用 new T() 除非有 where T : new() 约束
// 不能用 default 以外的字面量初始化
// 不能对泛型类型使用运算符(除非 .NET 7+ INumber<T>)

// .NET 7 之前的变通方案
T Add<T>(T a, T b)
{
    // return a + b;  // 编译错误
    dynamic da = a!;
    dynamic db = b!;
    return da + db;   // 运行时解析,有性能代价
}

// .NET 7+ 使用泛型数学
T Add<T>(T a, T b) where T : INumber<T> => a + b;

Console.WriteLine(Add(1, 2));       // 3
Console.WriteLine(Add(1.5, 2.5));   // 4.0

11.7 面试要点

  1. CLR 泛型和 Java 泛型的区别?

    • CLR 是真泛型(运行时保留类型信息),Java 是类型擦除
    • CLR 值类型泛型无装箱,Java 必须用包装类
  2. 值类型和引用类型的泛型代码生成有什么区别?

    • 值类型:每种类型生成独立的 JIT 代码(类型特化)
    • 引用类型:共享同一份 JIT 代码
  3. 什么是协变和逆变?

    • 协变(out):子类型可以赋值给父类型,T 只能在输出位置
    • 逆变(in):父类型可以赋值给子类型,T 只能在输入位置
  4. 泛型类的静态字段是共享的吗?

    • 不是。每个封闭泛型类型有独立的静态字段
  5. where T : new() 约束的作用?

    • 保证 T 有无参构造函数,允许在泛型方法中使用 new T()

练习

  1. 实现一个泛型对象池 ObjectPool<T> where T : new()
  2. 利用泛型静态字段实现一个高性能的类型元数据缓存
  3. 实现一个支持协变的只读集合接口
  4. 使用 .NET 7 的泛型数学(INumber<T>)实现一个通用的统计函数(求和、平均值、标准差)

← 上一章:反射与 Source Generator | 下一章:依赖注入的原理与实现 →

评论

登录 后发表评论

暂无评论