泛型协变与逆变理解

协变(covariant)、逆变(contravariant)与不变

这个概念一般是编程语言中类型系统(尤其是泛型)中的概念,主要用来描述父子类型在使用中是否允许替换。

1
2
3
4
5
6
7
8
逆变与协变描述的是类型转换后的继承关系。定义 A、B 两个类型,A 是由 B 派生出来的子类(A<=B)

协变:当 A<=B 时,需要使用B(父类型)的地方可以用A(子类型)替换

逆变:当 A<=B 时,需要使用A(子类型)的地方可以用B(父类型)替换

不变:当 A<=B 时,不允许类型的互相替换

为什么需要协变与逆变

在编程语言的泛型设计中,一般都会讨论协变与逆变。提供协变与逆变的支持,使得泛型的使用更加具备灵活性。编程语言对逆变与协变的支持,使得开发者能够安全的使用类型替换,从而更加灵活地完成自己的编码需求。

java 中的逆变与协变

java 语言设计本身有继承,类类型之间都有典型的父子关系。java 中的逆变与协变发生在:

  • 函数入参、返回值:入参是协变的,返回值是逆变的
  • 泛型:泛型支持<? extends T> 的逆变和 <? super T>的协变

泛型何时使用<? extends T>,何时使用 <? super T>

这个可以参考 effective java 中提到的 PECS (producer extends, consumer super)。java 泛型中的逆变是使用更加具体的子类型,当类型作为生产者时,我们关注更加具体的类型。类型作为消费者时则使用逆变,因为已经处理完毕,我们只关心接受的父类型。其实理解生产者和消费者有个简单的办法:

  • 生产者类型:类型关联的对象还没有被处理,所以要用更加具体的子类型,例如函数入参
  • 消费者类型:类型关联的对象已经被处理,返回结果我们只要用不太具体的父类型即可,更加灵活,例如函数返回值。

rust 类型中更加广义的逆变与协变

逆变与协变其实不仅仅局限在父子类型这种关系序列中。其实任何具备一定包含关系的场景中都可以使用逆变与协变,即使类型没有继承关系。例如在 rust 语言中,类型生命周期有长短之分。在 rust 中生命周期更长的类型可以理解为更加“具体”,是子类型。rust 生命周期是否支持安全的协变与逆变有很多场景,有兴趣可以看 rust 死灵书相关章节了解下。

参考资料: