一、协变与逆变的核心概念
1. 定义与公式
-
协变(Covariance) :允许将派生程度更高的类型(子类)隐式转换为派生程度较低的类型(父类)。
公式 :IFoo<父类> = IFoo<子类>
示例:
IEnumerable<Person> persons = new List<Student>();
类比 :子类对象可安全视为父类对象(如Student
→Person
),符合直觉的"向上转型"。 -
逆变(Contravariance) :允许将派生程度较低的类型(父类)隐式转换为派生程度更高的类型(子类)。
公式 :IBar<子类> = IBar<父类>
示例:
Action<Student> studentAction = (Action<Person>)p => { };
类比 :父类处理逻辑可安全用于子类对象(如Person
处理器处理Student
),符合"更宽泛的输入"。
2. 设计目的
- 解决泛型类型转换限制 :
传统泛型(如List<T>
)不支持子类与父类集合的隐式转换(如List<Person> persons = new List<Student>()
编译失败)。
协变/逆变通过类型参数修饰符(out
/in
)扩展兼容性,增强代码灵活性。
二、实现机制与关键字作用
1. 类型参数修饰符
-
out
关键字(协变) :- 约束泛型参数仅作为输出位置(返回值)。
- 确保类型安全:子类返回值可安全视为父类。
示例接口:
csharppublic interface IEnumerable<out T> { IEnumerator<T> GetEnumerator(); // T 仅作为返回值 }
-
in
关键字(逆变) :- 约束泛型参数仅作为输入位置(方法参数)。
- 确保类型安全:父类参数可安全处理子类输入。
示例接口:
csharppublic interface IComparer<in T> { int Compare(T x, T y); // T 仅作为参数 }
2. 底层安全原理
-
协变安全性 :
IEnumerable<out T>
仅支持读取(如GetEnumerator
),无法添加元素,避免向List<Student>
插入Person
导致类型错误。 -
逆变安全性 :
IComparer<in T>
仅消费输入(如Compare
),不返回T
,避免将Person
强制转为Student
。
三、支持协变/逆变的常见类型
1. 协变接口(out T
)
接口 | 说明 | 示例应用场景 |
---|---|---|
IEnumerable<T> |
只读集合遍历 | 多态集合处理 |
IQueryable<T> |
LINQ 查询数据源 | 跨层级数据查询 |
IReadOnlyList<T> |
不可变列表 | 安全传递子类集合 |
Func<out TResult> |
无参返回值委托 | 工厂方法委托 |
2. 逆变接口/委托(in T
)
类型 | 说明 | 示例应用场景 |
---|---|---|
IComparer<T> |
对象比较器 | 父类比较器用于子类排序 |
Action<in T> |
单参数无返回值委托 | 通用事件处理器 |
Predicate<in T> |
条件判断委托 | 过滤不同子类对象 |
IEquatable<T> |
对象相等性比较 | 跨层级对象比对 |
四、典型应用场景与代码示例
1. 协变:集合多态处理
kotlin
class Person {}
class Student : Person {}
// 协变允许子类集合视为父类集合
IEnumerable<Person> people = new List<Student>();
foreach (Person p in people) { /* 安全操作 */ }
意义:统一处理多种子类集合,无需强制转换。
2. 逆变:泛型委托兼容
ini
// 逆变允许父类委托接受子类输入
Action<Person> logPerson = p => Console.WriteLine(p.Name);
Action<Student> logStudent = logPerson; // 逆变转换
logStudent(new Student()); // 安全执行
意义:复用父类逻辑处理子类,减少重复代码。
3. 混合变体:Func
委托
ini
// Func<in T, out TResult> 支持参数逆变 + 返回值协变
Func<Student, string> getStudentName = s => s.Name;
Func<Person, object> getPersonInfo = getStudentName; // 安全转换
object info = getPersonInfo(new Student()); // 返回 string → object
意义:最大化委托灵活性,适配不同输入/输出类型。
五、重要限制与注意事项
-
仅支持引用类型 :
值类型(如
int
)不支持协变/逆变,因值类型转换涉及装箱拆箱。 -
修饰符约束:
out T
禁止作为方法参数(避免写入)。in T
禁止作为返回值(避免读取)。
违反将导致编译错误。
-
不适用于普通类 :
仅泛型接口、委托、数组支持变体(如
List<T>
无修饰符,故不支持协变)。 -
数组协变的风险:
iniobject[] arr = new string[10]; arr[0] = 123; // 编译通过,运行时抛出 ArrayTypeMismatchException
需谨慎操作避免类型错误。
六、总结
- 协变 :通过
out T
实现子类→父类转换,用于输出场景(如集合遍历、工厂委托)。 - 逆变 :通过
in T
实现父类→子类转换,用于输入场景(如比较器、处理器委托)。 - 核心价值:在保证类型安全的前提下,增强泛型类型的兼容性,提升代码复用性与扩展性。
通过合理应用变体特性,可显著简化多态设计(如统一处理异构集合、构建灵活委托链),是高级泛型编程的核心工具之一。