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
}
上一篇 下一篇

猜你喜欢

热点阅读