一些定义
考虑类型A
和C
,以及泛型构造器F<T>
:
- 如果我们在一个需要
A
的地方,总能使用C
,我们就可以说C
是A
的子类型(subtype)[1],记作C <: A
。 - 如果F是协变的(covariant),且
C <: A
,则有F<C> <: F<A>
。 - 如果F是逆变的(contravariant),且
C <: A
,则有F<A> <: F<C>
。 - 如果F是不变的(invariant),无论
A
与C
是什么关系,F<C>
与F<A>
都没有关系。
Collection视角
先举用例子来看看Collection的型变规则。
Duck
是Bird
的子类,Bird
是Animal
的子类,记作Duck <: Bird <: Animal
。
TypeScript中的Array
是协变的(covariant):
1 | interface Animal { name: String } |
函数传入参数同样遵循这个规则:
1 | function birdsFly(birds: Array<Bird>) { |
默认情况下,Java的List
是不变的(invariant):
1 | List<Bird> birds = new ArrayList<Duck>(); // Illegal |
当然,我们可以用bounded wildcards在使用时去除不变性:
1 | List<? extends Bird> coList = new ArrayList<Duck>(); // Legal; covariant |
Effective Java中提到的PECS(producer extends,consumer super),就是说,如果你接收一个Collection:
- 只读其中的元素(Collection是producer),那么你最好接受一个协变的Collection;
- 反之,如果你只写入
List
(Collection是consumer),那么你最好接受一个逆变的Collection。
1 | void namesOfBirds(List<? extends Bird> birds) { |
Function Type
容器类型的型变其实是比较符合直觉的,函数类型就很反直觉了。
1 | trait Function1[-T1, +R] |
这是Scala标准库定义的单参函数的trait(可以理解为interface),类型为T1 -> R
,其中T1
是逆变的,R
是协变的[2]。
But why???
回想一下,型变规则决定了我们能使用什么样的子类型,子类型决定了我们能在一个期望X的地方,安全地使用另一个类型Y(X的子类型)。
So,既然函数类型也是一种类型,我们当然也要知道函数类型的子类型咯,对吧?
那么对于函数类型Bird -> Bird
,根据trait定义,Animal -> Duck
就是它的子类型。
Weird, right?
假设我们有高阶函数f: (Bird -> Bird) -> String
(返回什么我们并不关心)。
将传入的函数记作g
,尝试在g
的参数上用子(超)类型进行替代:
1. 如果g: Duck -> Bird
,f(g)
会怎样?
f
已知g
接受Bird
,于是f
调用g
的时候,传给g
一个Bird
。
然而,g
只能处理Bird
的一个子类型Duck
,如果接收到一个Swan
,g
会崩溃。
所以参数类型不可以用子类型替代。
2. 如果g: Bird -> Animal
,f(g)
会怎样?
f
已知g
返回Bird
,于是f
会将g
的返回值当作Bird
来使用。
但是现在g
会返回一个不是Bird
的Animal
,比如Dog
,那么f
会崩溃。
所以返回类型不可以用超类型替代。
3. 如果g: Animal -> Duck
,f(g)
又如何呢?
f
已知g
接收Bird
,所以传给g
一个Bird
,g
可以正常处理Bird
并返回Duck
,没有问题。
f
已知g
返回Bird
,所以将g
返回的Duck
当作Bird
来用,所有Duck
都是Bird
,也没问题。
所以,在一个期望(Bird -> Bird)
的地方,我们总是可以使用(Animal -> Duck)
。
即(Animal -> Duck)
是(Bird -> Bird)
的子类型。
也就是说,参数类型可以用超类型替代,返回类型可以用子类型替代。
形式化一点,就是对于函数类型构造器->
,接收参数逆变且返回参数协变时,仍然可以保证类型安全。
或者说:对于函数p和q,当p的参数更general,返回值更specific时,p可以安全地替代q。
其实,Java8的Function
接口的型变规则也是这样的,compose
和andThen
都是接收了一个Function<? super V, ? extends T>
,恰恰正是参数逆变,返回值协变。
1 | // see http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/function/Function.java |
References
- What are covariance and contravariance?
- Covariance and contravariance (computer science)
- Covariance, contravariance and a little bit of TypeScript
- Covariance and contravariance rules in Java
- Variances - Tour of Scala
- Functoriality - Categories for Programmers