谈谈 Variance
Variance 是一个计算机科学中的术语,简单来说,如果 A 和 B 两个类型有某种关系,比如 A 是 B 的子类型,那么 Type<A>
和 Type<B>
之间的关系是什么?
举个具体的例子,Cat
是 Animal
的子类,那么 List<Cat>
和 List<Animal>
的关系呢?凭直觉看,一群猫当然也是一群动物了。从 是
的角度看,这么想当然没错,只是实际编程中,需要从更多的角度来看这个问题。
本文主要通过 Java 和 Kotlin 两种语言来描述这个问题,以及为了实现 Type<A>
和 Type<B>
的关系,需要用到哪些语言特性。
关于 Variance 一般有这么 3 种结果:
-
Covariance
Type<A>
也是Type<B>
的子类型,类型关系一致 -
Contravariance
Type<B>
是Type<A>
的子类型,与原类型关系相反 -
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[]
保持了 Cat
与 Animal
的关系,即 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>
的关系。
Cat
是 Animal
,所以 List<Cat>
似乎也是 List<Animal>
?但是在编程中,是
有更多的含义。
举个具体的例子,在一群动物中,放进一只猫,仍然可以看作是一群动物;但如果在一群猫中,放入一只狗,就不能再当作一群猫了。
所以 List<Cat>
和 List<Animal>
之间的关系,在读的时候和写的时候,是不一样的。因为 List<Animal>
支持放入 Dog
,但是 List<Cat>
并不支持,所以干脆编译期间报错。
从读和写的角度考虑这段继承关系: Creature ----> Animal ----> Cat
- 读,实际提供类型越往右,是一定安全的,具体的子类一定符合要求;但是往左是不安全的,临界点就是
Animal
- 写,实际提供类型越往右,越不安全,因为继承关系不是链表,而是树,要求
Animal
传入Cat
,会面临Dog
等其他继承分支不能匹配的问题;反之越往左,因为是父类,Cat / Dog
这些具体子类都满足类型。
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
。