1. 泛型本质(CLR 底层视角)
定义:泛型是类型参数化机制。在定义时不指定具体类型,用占位符(如
T)代替;在实例化/调用时传入具体类型。CLR 执行时机:泛型是运行时(JIT)真正落地的。
值类型(struct):JIT 会为每一种不同的值类型参数生成一份独立的本机代码(例如
List<int>和List<long>是两份完全不同的机器码)。引用类型(class):JIT 会共用一份本机代码(例如
List<string>、List<object>、List<Stream>共用同一份代码,因为引用类型指针大小固定,占位由运行时替换)。
核心性能收益:避免值类型的装箱/拆箱(零开销抽象),同时保留编译时类型检查。
2. 泛型种类(语法落点)
泛型可以附着在5种类型上:
| 附着对象 | 示例 | 说明 |
|---|---|---|
| 泛型类 | class MyClass<T> { } | 最常用,类级全局占位 |
| 泛型结构 | struct MyStruct<T> { } | 值类型也能泛型化 |
| 泛型接口 | interface IMy<T> { } | 约束行为,支持协变/逆变 |
| 泛型方法 | void MyMethod<T>(T arg) { } | 不必依赖泛型类,可单独存在 |
| 泛型委托 | delegate T MyDelegate<T>(T arg); | 如Func<T, TResult> |
高频坑:泛型方法可以存在于非泛型类中,且编译器能通过参数自动推断类型(不必显式写
<T>)。
3. 类型约束(where子句)—— 面试必问
约束用于限制T必须满足的条件,不约束则只能使用object的方法(ToString、Equals、GetHashCode)。
| 约束写法 | 含义 | 关键限制 |
|---|---|---|
where T : struct | T 必须是值类型(不可为 null) | 不能是Nullable<T>(如int?) |
where T : class | T 必须是引用类型 | 可以是接口、类、数组等 |
where T : class?(C# 8+) | T 是可空引用类型 | 主要用于可空上下文 |
where T : notnull(C# 8+) | T 是不可空类型(值或引用) | 排除string?和int? |
where T : unmanaged(C# 7.3+) | T 是非托管类型(不含引用) | 常用于Span<T>、内存操作、P/Invoke |
where T : new() | T 必须有无参构造函数 | 必须放在约束链的最后一位 |
where T : BaseClass | T 必须是基类或派生类 | 可调用基类成员 |
where T : IInterface | T 必须实现指定接口 | 可调用接口方法 |
where T : U | T 必须继承自另一个泛型参数 U | 极少用,解决类型关联 |
组合约束示例(含顺序):
csharp
class MyClass<T> where T : class, IComparable, new() // class在前,new()必须在最后 { }4. 静态成员与泛型 —— 最大认知盲区
结论:泛型类的静态成员,是按“闭合类型”独立存储的。
MyClass<int>.StaticValue和MyClass<string>.StaticValue是两个完全独立的静态变量,互不影响。面试经典题:
csharp
class MyClass<T> { public static int Count; } MyClass<int>.Count = 10; MyClass<string>.Count = 20; Console.WriteLine(MyClass<int>.Count); // 输出 10(与string的20毫无关系)原因:JIT 在运行时为每个不同的闭合类型生成不同的静态字段内存区域。
5. 协变(out)与 逆变(in)—— 高级多态
仅适用于泛型接口和泛型委托,不适用于泛型类。
| 关键字 | 名称 | 作用方向 | 示例接口 | 允许操作 |
|---|---|---|---|---|
out | 协变 | 返回值(产出) | IEnumerable<out T> | 允许IEnumerable<string>赋给IEnumerable<object> |
in | 逆变 | 参数(消费) | Action<in T> | 允许Action<object>赋给Action<string> |
记忆口诀:out 是产出(返回值),in 是收入(参数)。
重要限制:协变/逆变只能用于引用类型,值类型不生效(IEnumerable<int>不能转IEnumerable<object>)。
6.default关键字在泛型中的妙用
在不知道T具体是值还是引用时,用default(T)或简写default获取默认值:
值类型:返回 0、
false或空结构体。引用类型:返回
null。
7. 泛型与反射/类型获取
获取未绑定泛型(开放类型):
typeof(MyClass<>)(不留占位符)。获取已绑定泛型(闭合类型):
typeof(MyClass<int>)。运行时创建泛型实例:
csharp
Type type = typeof(MyClass<>).MakeGenericType(typeof(string)); object obj = Activator.CreateInstance(type);
8. 泛型约束的局限性(必须知道的坑)
不支持运算符重载约束:无法约束
T必须有+或*运算符,因此泛型数学运算很麻烦(需借助System.Numerics.INumber<T>接口(C# 11+ .NET 7+))。不支持枚举约束:不能写
where T : enum(.NET 早期限制,现可用where T : struct, Enum绕行)。不支持委托约束:不能写
where T : Delegate(同样可用where T : class?绕行,但无法约束具体委托类型)。
9. 性能特别提示(面试加分项)
数组协变有风险:
object[] arr = new string[10];这种写法是数组协变(非泛型),运行时赋值int会抛出异常。泛型集合List<T>是安全的,强烈替代数组协变。List<object>与List<string>不是父子关系(不变性),必须通过IEnumerable<out T>实现协变。
10. 终极避坑口诀(泛型版)
值类型泛型:独立机器码,零装箱。
引用类型泛型:共享机器码,省内存。
静态成员:闭合成一家,相互不干扰。
out产出可向上(子转父),in收入可向下(父转子)。new()约束:必须垫底写。default不迷茫,值零引用空。