在 C#
泛型和委托的世界里,有两个经常让人皱眉的词:协变(Covariance) 和 逆变(Contravariance)。
它们的名字很抽象,但作用很实际:
它们允许你在泛型类型之间进行“安全”的类型替换,而不需要写一堆类型转换代码。
1. 为什么会有协变和逆变
先看一个简单例子:
1 | class Animal { } |
为什么 IEnumerable<Animal>
可以指向 List<Cat>
,而 List<Animal>
不行?
答案就是:协变和逆变是编译器提供的“类型兼容规则”,而 IEnumerable<T>
支持协变,但 iLst<T>
不支持。
2. 基础概念
2.1 协变(Covariance)
方向:子类型 → 父类型
允许将泛型参数类型的派生类,赋值给泛型参数类型的基类。
在
C#
里,用out
修饰泛型类型参数来支持协变。常见于 只读 场景(生产数据)。
1 | IEnumerable<Cat> cats = new List<Cat>(); |
Mermaid 图示:
classDiagram class Animal class Cat Animal <|-- Cat class IProducer~out T~ class IProducer~Animal~ class IProducer~Cat~ IProducer~Animal~ <|-- IProducer~Cat~ : 协变(out) IProducer~out T~ : +Produce() T IProducer~out T~ <.. Animal IProducer~out T~ <.. Cat
2.2 逆变(Contravariance)
方向:父类型 → 子类型
允许将泛型参数类型的基类,赋值给泛型参数类型的派生类。
在
C#
里,用in
修饰泛型类型参数来支持逆变。常见于 只写 场景(消费数据)。
1 | Action<Animal> actAnimal = a => Console.WriteLine(a); |
Mermaid 图示:
classDiagram class Animal class Cat Animal <|-- Cat class IConsumer~in T~ class IConsumer~Animal~ class IConsumer~Cat~ IConsumer~Cat~ <|-- IConsumer~Animal~ : 逆变(in) IConsumer~in T~ : +Consume(T item) IConsumer~in T~ <.. Animal IConsumer~in T~ <.. Cat
3. out 和 in 关键字
3.1 out(协变)
1 | public interface ICovariant<out T> |
约束:泛型参数
T
只能作为返回值(输出位置)。原因:如果能接收参数,就可能写入错误类型(破坏类型安全)。
3.2 in(逆变)
1 | public interface IContravariant<in T> |
约束:泛型参数
T
只能作为方法参数(输入位置)。原因:如果能返回
T
,调用方期望更具体类型时会出错。
4. 协变 & 逆变 代码实战
4.1 协变场景
1 | public interface IProducer<out T> |
要点:IProducer<T>
中 T
只出不进,所以 Cat → Animal
合法。
4.2 逆变场景
1 | public interface IConsumer<in T> |
要点:IConsumer<T>
中 T
只进不出,所以 Animal → Cat
合法。
4.3 协变 + 逆变 同时使用(Func 委托)
1 | Func<Animal, Cat> func = a => new Cat(); |
Func<in T, out TResult>
本身就是协变+逆变的复合例子。
5. 协变逆变的限制与注意点
只能用于接口和委托
类的泛型参数不能用in / out
。不能破坏类型安全
协变接口不能在输入位置使用泛型参数,逆变接口不能在输出位置使用泛型参数。数组是天然协变的(但是有坑)
1 | Animal[] animals = new Cat[10]; // 编译通过 |
结论:数组协变是运行时检查,泛型协变是编译时检查,后者更安全。