一、什么是协变和逆变
如果存在一个泛型接口Interface<T>
它的泛型参数的子类型IInterface<Dog>可以安全的转换成泛型父类型的IInterface<Animal>
这个过程就称为协变
如果存在一个泛型接口IInterface<T>
它的泛型参数的父类型IInterface<Animal>可以安全的转换成泛型子类型的IInterface<Dog>
这个过程就成为逆变
我们先明确两个术语:
协变(Covariance):保持类型顺序。如果 Dog 是 Animal 的子类,那么 IEnumerable<Dog> 可以被视为 IEnumerable<Animal>
逆变(Contravariance):反转类型顺序。如果 Dog 是 Animal 的子类,那么 Action<Animal> 可以被视为 Action<Dog>
协变与逆变解决的问题
假设你有如下类层次:
csharp
class Animal { }
class Dog : Animal { }
❌ 没有协变/逆变时的问题
在没有协变的情况下,以下代码会编译失败:
csharp
List<Dog> dogs = new List<Dog>();
// 编译错误!List<Dog> 不能赋值给 List<Animal>
List<Animal> animals = dogs;
因为泛型默认是 不变(Invariant) 的,即 List<Dog> 和 List<Animal> 没有继承关系。
但直觉上,狗的列表"应该是"动物列表的一种。协变就是为了解决这种合理的类型转换需求,同时保证类型安全。
实际应用场景
协变: 你有一个 IReadOnlyList<IDocument>,但实际传入的是 IReadOnlyList<PdfDocument>,协变让它可以接受。
逆变: 你有一个排序器IComparer<object>,它可以用于比较任何对象,包括 string 或 Person,所以可以赋值给 IComparer<string>
C# 使用 out 和 in 关键字来声明协变和逆变:
out T:表示 协变(Covariance),T 只能作为方法的返回值(输出)。
in T :表示 逆变(Contravariance),T 只能作为方法的参数(输入)。