白话类型系统中的协变与逆变

静态类型检查是一个强大的工具,基本目的就是帮助程序员在编译期发现代码中的一部分问题。

支持静态类型检查的语言很多,例如C++,Java,Scala等。当然,有些语言不支持静态类型检查也照样火的不得了,例如python同学。不过我个人认为静态 类型检查对大型项目的构建非常必要的。

静态类型检查的基本原理就是一个萝卜一个坑。如果把一个白菜放到了萝卜坑里,编译器就给你报个错。正经点说就是检查引用和对象的类型是否匹配。

如果有继承关系的话,父类的引用可以指向子类的引用。例如,白萝卜可以放到萝卜坑。萝卜不可以放到白萝卜坑,因为这个萝卜可能是胡萝卜。

之所以这么定,是因为我们规定萝卜的各种做法白萝卜必须支持,而白萝卜的各种做法萝卜不一定支持(里氏替换原则,Liskov Substitution Principle, LSP)。 在我们做萝卜坑里的食物时候,如果它是个白萝卜,这是安全的;而我们做白萝卜坑里的食物时候,如果它只是个萝卜,是不安全的,因为可能用到一些萝卜不支持 的做法。

这里强调一下可能二字,静态类型检查要杜绝这种可能发生的错误。

So far so easy。

下面我们来考虑复合类型,所谓复合类型是指某个东西具有两种或两种以上的类型,例如一个拿着萝卜的人。它有两个类型,人和萝卜。

假如这里有个座位是给拿着萝卜的人坐的,拿着白萝卜的人能不能做呢?

乍一看上去是可以的。但我们设想一下,如果人要用萝卜做一道菜,他和萝卜的关系是一个消费关系。拿着白萝卜的人,可能会做一些只适合白萝卜的菜。 如果他问我们要萝卜做一个菜,我们给他一个胡萝卜,这是合法的,但是他做菜的时候就出问题了。所以这种情况下,拿着白萝卜的人不能坐在标示为拿着萝卜的人的位置上。

那么什么情况下拿着白萝卜的人可以坐呢?如果这个人保证我只卖萝卜,我和萝卜只有生产关系。这种情况下是可以坐的。因为我们想从他那里得到萝卜,而他提供的白萝卜是一种萝卜。

如果允许拿着白萝卜的人坐在标记为拿着萝卜的人的位置上,称之为协变。我们看到,如果在类的所有方法里面,主类型对于嵌入的类型只有生产关系,是可以协变的。

下一个问题,一个拿着萝卜的人,能不能坐在标记为拿着白萝卜人的位置上呢?

如果人和萝卜有生产关系的话,是不行的。因为要是让他坐下了,我问他要白萝卜的时候,他可能给一个胡萝卜,这就有问题了。

但是如果任何萝卜只有消费关系的话,是可以的。他只会用一些萝卜的做法,我们提供给他白萝卜也好,胡萝卜也好,都不会有问题的。

如果允许拿着萝卜的人坐在标记为拿着白萝卜的位置上,称之为逆变。我们看到,如果在类的所有方法里面,主类型对于嵌入的类型只有消费关系,是可以逆变的。

如果类的所有方法里,与嵌入类型既有生产关系,又有消费关系,那怎么办?那就只能要求复合类型完全一致,这叫不变

Java/Scala里面默认的对于复合类型的静态类型检查都是不变的。但也都提供了方法去修改,例如<? extends T>, <? super T>, [+T], [-T]。

在《Effective Java》这本书里有一个PECS的原则,即主类型和嵌入类型之间只有生产关系可以用extends,只有消费关系可以用super。