泛型编程中的型变
在泛型编程中,经常会提到型变。型变分为两种:协变与逆变。协变covariant表示与泛型参数T的变化相同,而逆变contravariant则说明与泛型参数T的变化相反。
怎么理解呢?这就涉及到面向对象编程中的一个基本的基本的架构:父类与子类的关系。我们可以针对父类(或者接口,可以勉强将接口理解成父类,虽然这并不严谨)编程,然后针对父类进行编程,然后这段程序就可以处理子类的。比如我们的函数,接入一个父类的指针,在使用这个函数的时候,就可以给他传一个派生类(子类)参数。对于返回值来说,也是类似,函数声明返回一个父类型,你可以返回任意的子类型对象。
R Function(T t) ; 其中R为返回类型,T为参数类型。
一般我们写一个函数的时候,其中是不用管协变逆变的,这是因为我们理解(或者说默认)子类型是对父类型兼容的,当我们的代码中写下一个类型T的时候,我们知道这里其实要求的是T类型的兼容类型,也就是T类型或者它的子类型。
那么什么时候,我们才会需要注意到这些事情呢。那就是泛型的引入。仅仅引入泛型,比如
class Array[T] {};
我们还不用太关心型变,比如我们就可以
Array[int] * a = new Array[int];
在代码中直接实例化就好了,也不用管型变不型变的。但是一旦涉及到泛型实例化之后的兼容性问题,就涉及到型变了。泛型引入之后,它带来的问题是不同的泛型实例化之后,是不同的类型系统,两者可以认为完全没有任何关系,比如
type Array[int] 与 type Array[long] 是两种不同的类型,它们并不兼容(至少从编译器的角度来说)。甚至 type Array[父类型]与type array[子类型]也是不一样的类型。我们不能
Arrary[int] * a = new Array[long]; // oops
也不能
Array[父类型] *a = new Array[子类型]; // oops too!
再加上泛型函数,同样如此
function [T] doSomething(T p) {... }
那么doSomething[int]与doSomething[long]不是兼容类型,同样的doSomething[父类型]与doSomething[子类型]并不兼容。
可是我们就是有的时候,想要兼容啊,比如接受泛型类型作为参数的时候,我们有两种基本的模式: 一种是C语言那种,只是一个指针,你随便玩,这是一种没有类型检查自己负责的方式。一种就是我们在代码中,就指定了类型,比如
function [T] typedPrint (T a, function [T] printer(T a1) f) { f(a); }
这函数没有啥用,就是打印一个变量,但是我们可以使用不同的打印策略。问题来了,对于参数a,我们可以传入子类型,因为子类型是兼容的,但是printer[子类型]却与父类型不兼容,所以我们现在typePrint只能针对特定的类型来写,没有办法跟继承体系玩了。
为了能够对于这种基于泛型建立的类型(泛型容器、泛型函数、泛型接口等等)之间建立某种兼容性的说明,我们才通过声明型变方向,建立这种联系,看起来就象是建立了类似某种继承关系似的。
- 协变就是跟随基本类型一致,比如Type[子类]与Type[父类]兼容。
- 逆变就是与基本类型变化的方向相投,比如Type[父类]与Type[子类]兼容。
什么时候需要协变,什么时候需要逆变呢,一个基本的原则是:如果是读,就是协变的,如果是写,就是逆变的。我们以下面一个函数来做举个例子:
function [T, R] void Demo(T z, Type[T] a, function [R, T] R func(T v));
大概的意思就是,某个泛型类Type[T]的变量a,以及一个处理函数func,处理函数的入参是T类型,而返回值是R类型)。整个Demo函数的功能,是从a中取出T类型的参数,交给func处理,func处理之后,返回一个R类型。
Demo函数的难度在于,一旦T的类型定义了,于Type[T]与 function [R, T] R func(T v)的类型也限定了。现在我们函数的主义,是我们需要从Type[T] a里面读数据出来,所以只要是协变的可以了,我们能够从Type[父类]与Type[子类]中都能够读出来父类型的,所以这里规定协变即可,我们借用scala的表示法
function [T, R] void Demo(T z, Type[+T] a, function [R, T] R func(T v));
反过来,如果在函数里面,我们要往a里面写数据,比如将z保存到a里面去,那么对于a的要求就必须是Type[父类型],这里候是逆变的。
function [T, R] void Demo(T z, Type[-T] a, function [R, T] R func(T v));
表示要求现在的a容器能够容纳z,所以肯定是一个父类容器。
函数其实也是类似,只是这里要把函数想象成一个容器,函数自己再要的参数,就相当于写到函数里去了,函数要求得到一个参数v,我们准备给它一个T类型的,所以对函数的要求是它能够处理T类型或者T类型的父类型。这与往容器里写数据的逻辑是类似的,所以它要求是逆变的。而函数的返回值,是我们获取它的返回值,相当于从函数读出来一个东西,所以就应该是协变的,必须是我们要求的类型或者子类型。
function [T, R] void Demo(T z, Type[+T] a, function [+R, -T] R func(T v));