网上随随便便就能搜到逆变和协变定义,所以本文也不再针对这方面做阐述。本文主要讲述逆变和协变在设计层面的考虑。
前提
网上有很多人都容易把逆变和协变与泛型相混淆。实际上逆变和协变并不属于泛型的范畴,虽然他们之间有很多关联的地方。不然的话你可以用泛型来解释协变,但是你怎么用泛型来解释逆变?逆变和协变主要源自与通配符(?)。而通配符的设计主要是为了代码复用。比如写个接受List(任意类型的list)作为参数的函数。总不能为String 类和Integer 类都设计一个参数吧,所以逆变和协变主要是为了约束通配符。
逆变和协变的设计思想
设计的目的是为了使用。关于逆变和协变的使用有以下几个原则:
-
协变只能出不能进。
这里的进不是说作为只能作为输入参数的意思,而是说发生协变的集合只能从里面取出来数据。不能在里面放入数据。
比如下面几个三个类:abstract class Fruit { } class Apple extends Fruit { } class Orange extends Fruit { }
然后我们有一个函数(这个函数是报错的):
void add(List<? extends Fruit> fruits, Fruit fruit) { fruits.add(fruit) }
对于变量
List<? extends Fruit> fruits
,这个fruits 可以是List<Orange>
或者List<Apple>
。但是具体是什么是不知道的。现在我们把fruit放进去。如果我们fruits
的类型是List<Apple>
,但是fruit是个Orange
。那我们的程序就会报错。但是我们取出来用的时候知道无论存的是什么,都是Fruit
的子类,Fruit
有的属性和函数集合里的元素都有,拿来用的时候怎么用都不会报错。所以说:协变只能出不能进。虽然说协变只能出不能进,但是不是说协变只能用于集合的输出。而是说协变的对象一般是被拿来使用的,不能被修改。 -
逆变能进又能出
假如有个List<? super Apple> apples
变量。这个变量里面可以定义为可以存放Apple
及Apple的父类(只能是一个,不能说同时存放多个没有关系的父类,一个集合必须是确定的)。那在里面存放Apple
不会有任何问题,但是如果存别的东西就可能出问题。但是取出来使用的时候会有问题。因为存和取是隔离的。取得时候并不知道当初存的是什么玩意,只知道存的是Apple
的父类。所以取出来之后不能滥用,因为有可能Apple
对象有的方法,集合里的元素没有。所以从逆变集合里取出来的对象只能当做Object来使用。难道说逆变存在的意义只是可以存入数据和把集合里面的元素拿出来当做Object来使用吗?当然不是。因为如果实现明白发生协变的原始类型,可以使用强制类型转换得到原始类型。
在scala关于接收两个参数,返回一个参数的的函数具有相同的特质(java里的接口)定义如下:
trait Function2[-T1, -T2, +R] extends AnyRef
无论是协变还是逆变都是针对接受者而言。对于scala里的函数而言,如果一个函数要想赋值给另一个函数的指针(无论是直接赋值还是当做函数传参),函数的参数必须是指针(函数的指针)参数相同的类型或者父类型,返回值必须是指针(函数的指针)返回值的相同类型或者子类型。比如说有一个指针f1:
f1 : (Son1) => Father 2 // Son1 extend Father1,Son2 extend Father2
然后有一个函数f2:
f2 : (father1) => son2
现在把f2赋值给f1是没有问题的。
f1 = f2
为什么可以这样?
我们先约定一下称谓。我们把(Son1) => Father 2
称作f1的定义类型。当f1被f2赋值后,f1的类型是(father1) => son2
我们称之为f1的运行类型。使用者使用被赋值后的f1时只知道f1的定义类型,而不知道f1的运行类型(只有jvm知道)那我们只会传给被赋值后的f1 Son1类型的参数,也只会把被赋值后的f1的返回值当做Father2来用。其中内在的赋值逻辑就是:
- 对于参数:f1定义的类型(使用者传给函数的)-> f1运行时的类型
- 对于返回值:f1运行后的返回值类型 -> f1 定义的返回值类型(使用者希望获得的类型)
假设我们现在要使用f1指向的函数,我们给传个Son1类型的参数,但是实际上f1代表的是f2类型的函数,也就相当于我们把Son1传给(father1) => son2
。根据泛型,这当然没有问题。j现在f2的参数是Son1的子类型,那显然是不行的。返回值也是这个道理。