谈谈 Variance

Variance 是一个计算机科学中的术语,简单来说,如果 A 和 B 两个类型有某种关系,比如 A 是 B 的子类型,那么 Type<A>Type<B> 之间的关系是什么?

举个具体的例子,CatAnimal 的子类,那么 List<Cat>List<Animal> 的关系呢?凭直觉看,一群猫当然也是一群动物了。从 的角度看,这么想当然没错,只是实际编程中,需要从更多的角度来看这个问题。

本文主要通过 Java 和 Kotlin 两种语言来描述这个问题,以及为了实现 Type<A>Type<B> 的关系,需要用到哪些语言特性。

关于 Variance 一般有这么 3 种结果:

  1. Covariance

    Type<A> 也是 Type<B> 的子类型,类型关系一致

  2. Contravariance

    Type<B>Type<A> 的子类型,与原类型关系相反

  3. Invariance

    Type<A>Type<B> 没什么关系,原类型关系丢失了

此处强烈不建议各位参考对应中文名称,其实翻译倒也比较准确,只是彼此之间极大的相似性容易引起大脑不适,不如用 co- / contra- / in- 前缀来对应。

绝大多数情况下,我们都是在泛型的语境中讨论 Variance 问题,这里先用数组做个简单介绍。

1private void foo(Animal[] animals) { }
2
3Animal[] animals = new Animal[1];
4foo(animals);
5
6Cat[] cats = new Cat[1];
7foo(cats);

对于接受 Animal[] 类型的方法 foo,同样支持 Cat[] 类型,也就是说 Cat[]Animal[] 保持了 CatAnimal 的关系,即 Covariance。

foo 实现稍作修改:

1private void foo(Animal[] animals) {
2    animals[0] = new Animal();
3}

尝试运行会抛出 ArrayStoreException,因为在 Cat[] 中尝试插入 Animal 类型。通过这个例子,我们也可以看出,Variance 除了讨论 是不是,还有类型安全问题。

换泛型试试:

1private void foo(List<Animal> animals) {
2    animals.set(0, new Cat());
3}
4
5List<Animal> animals = new ArrayList<>();
6foo(animals);
7
8List<Cat> cats  = new ArrayList<>();
9foo(cats);      // compile error

好吧,这次直接是编译期错误了,期望传入 List<Animal> 实际传入 List<Cat>,在编译期看来,这两者并没有什么关系,属于上面说的 Invariance

让我们再回头重新思考一下 List<Cat>List<Animal> 的关系。

CatAnimal,所以 List<Cat> 似乎也是 List<Animal>?但是在编程中, 有更多的含义。

举个具体的例子,在一群动物中,放进一只猫,仍然可以看作是一群动物;但如果在一群猫中,放入一只狗,就不能再当作一群猫了。

所以 List<Cat>List<Animal> 之间的关系,在读的时候和写的时候,是不一样的。因为 List<Animal> 支持放入 Dog,但是 List<Cat> 并不支持,所以干脆编译期间报错。

从读和写的角度考虑这段继承关系: Creature ----> Animal ----> Cat

Variance In Java

在 Java 中,分别从读和写两个角度解决了上面的问题。

1. Covariance

引入 upper bound wildcard 定义类型上界,表示为 <? extends T>,通过这种方式实现了 Covariance,即 List<Cat>List<? extends Animal> 子类型。

通过把之前的方法修改为:

1private void foo(List<? extends  Animal> animals) { }

就可以传入任何一种具体的 List,比如 List<Cat> / List<Dog>,一群猫一群狗都可以被看作是一群动物。但是,这并没有解决上面描述的在一群猫中,放入一只狗,就不能再当作一群猫的问题,所以这种 Covariance 是 ReadOnly 的,尝试 Write 会在编译期抛出异常,所以也只是用在方法的返回值。

因为任意一种 Animal 都是 Animal,所以作为返回值时,List<? extends T> 相比于 List<T> 范围更窄,所以一定安全。

2. Contravariance

引入 lower bound wildcard 定义类型下界,表示为 <? super T>,通过这种方式实现了 Contravariance,即 List<Animal>List<? super Cat> 的子类型。

原因也如上文提到的,子类永远满足父类的类型,所以 Cat 可以出现在继承链上游的任意位置。但是这引入的新问题是,从 Cat 到 Object 都满足这个类型,所以没办法声明具体类型的。所以代价是 WriteOnly,用于方法传入参数。

1private void foo(List<? super Cat> animals) {
2    animals.set(0, new Cat());
3}

Variance In Kotlin

Kotlin 中的 Variance 和 Java 只是语法层面的区别。关键字 out 对应 Covariance,in 对应 Contravariance。

1. Declaration-site

声明在 Class,对所有成员生效:

1interface Source<out T> {
2    fun nextT(): T
3}

2. Use-site

在 Class 层定义当然方便,但有时候可能同时存在多种 Variance 关系,所以可以在使用时具体指定。

1fun copy(from: Array<out Any>, to: Array<Any>) { ... }

3. Tips

值得一提的是,为了与 Java 相互调用,Kotlin 对于参数中的 out / in 关键字做了特殊处理,out T 会被自动编译为 <? extends T>in T 编译为 <? super T>。 大部分情况下都没什么问题,但在 Dagger 的 IntoMap / IntoSet 中,原本期望 T 实际传入 <? extends T> 会导致依赖缺失。

这种时候需要在对应位置 @JvmSuppressWildcards T