Scala中的协变与逆变
2018-05-29 本文已影响0人
AlienPaul
什么是协变和逆变
给出Student和Person两个类,其中Student为Person的子类,对于协变来说List[Student]是List[Person]的子类。相反,对于逆变来说List[Person]为List[Student]的子类。
协变和逆变的用途
一句话描述为:生产者使用协变,消费者使用逆变。
下面我们来解释为何这么使用。
生产者我们使用的场景为食品生产商。先定义如下两种食品:
class Food
class Meat extends Food
加入我们定义的食品生产商如下,采用类型不变:
class Producer[T] {
def produce: T = new T
}
我们只能这么调用:
val p1 = new Producer[Meat]
val f1 = p1.produce // f1为Meat类型
val p2: Producer[Food] = p1
val f2 = p2.produce // 看似f2为Food类型,但是Producer中的类型T是不可变的,这两行会编译错误
现实中,肉类的生产商是食品生产商。即肉类生产商是食品生产商的子类。
为了满足要求,我们将Producer定义为协变:
class Producer[+T] {
def produce: T = new T
}
val p1 = new Producer[Meat]
val f1 = p1.produce // f1为Meat类型
val p2: Producer[Food] = p1 // 此行合法。因为根据协变的定义,Producer[Food]是Producer[Meat]的父类,根据多态,可以使用父类引用指向子类对象
val f2 = p2.produce // f2实际为Meat类型,但是被当做Food类型看待
对于逆变来说可能不是这么容易理解,直接上消费者的例子
class Animal[T] { //这里先不写协变或者逆变
def eat(t: T) = println("ate") // 该动物把T类型食物吃掉
}
下面有两种动物:
val pig = new Animal[Food] //猪猪是杂食动物,吃什么都行
pig.eat(food)
val tiger = new Animal[Meat] // 老虎只吃肉
tiger.eat(meat)
对于这两种动物,如果我们只有肉的情况下,可以同时养活猪和老虎,但是如果只有普通食物,是没有办法养活老虎的。如果普通食物短缺我们可以把猪当做食肉动物来养,代码如下:
val a: Animal[Meat] = pig
a.eat(meat)
val b: Animal[Food] = tiger // 不合适,老虎不吃普通食物
根据多态,父类引用可以指向子类对象,即Animal[Meat]
可以指向Animal[Food]
类型。所以说Animal[Meat]
是Animal[Food]
的父类。根据定义,Animal的类型T为逆变。
经过分析,Animal类的定义需要完善为:
class Animal[-T] { //T为逆变
def eat(t: T) = println("ate")
}
一个看似自相矛盾的问题
假如我们自己实现一个List:
trait List[T] {
def add(t: T)
def get(i: Int): T
}
对于add方法来说是消费者,T应该为逆变,可是对于get方法来说是生产者,T该为协变才对。看起来自相矛盾。
add方法的t位于逆变的位置,因此这里可以使用E >: T,传入一个T的父类E。将T转化为逆变类型。为什么说这样可以转换成逆变类型呢?根据李氏替换原则,任何使用父类的地方都可以使用其子类替换。这里可以使用T的超类E进行替换,所以说这里为逆变点。
该自相矛盾的问题便可以迎刃而解:
trait List[+T] {
def add[E >: T](e: E)
def get(i: Int): T
}