协变和逆变

协变就是正向的形变: B extends A 可以推导出 List<B> extneds List<A>

逆变就是逆向的形变: B extends A 可以推导出 List<A> extends List<B>

具体可以参考:《Effective Java》PECS 原则 (producser-extends, consumer-super)

G[+A]类似一个生产者,提供数据。(大部分情况下称G为容器类型)
G[-A] 是一个消费者,主要用来消费数据。(如上的 Equiv[-A] (其实就是个A => Boolean的函数,即Function1[-A, Boolean]))
同理函数的参数为何声明为逆变,返回值为协变就好理解了
同理
class G[+A]{def fun[B >: A](x: B){}}
就可理解为,我声明的是一个容器类型,但是它也有处理(消费数据)能力
参数逆变:正是因为需要符合里氏替换法则,方法中的参数类型声明时必须符合逆变(或不变),以让子类方法可以接收更大的范围的参数(处理能力增强);而不能声明为协变,子类方法可接收的范围是父类中参数类型的子集(处理能力减弱)。
返回值协变:如果结果类型是逆变的,那子类方法的处理能力是减弱的,不符合里氏替换。
目的:
协变和逆变的引入,当然是为了高阶类型F[A], F[B]之间, 也能像低阶类型A, B那样能够有型变的能力,如果List[String] 和 List[Any] 没什么关系,显然对不起“Scala是面向对象的”
 
上面的图,左边是逆变:
如果Bamboo extends Vegatable extends EnergySource
那么自然可以处理可以处理Bamboo的Consumer,自然也可以处理Vegetable和EnergySource,
可以处理Vegetable的Consumer,自然也可以处理EnergySource。
此时,Consumer可以处理的对象和被处理对象继承方向是相反的。
 
Scala的编译器会自动的进行下面3项的check:
1. 逆变只能出现在方法或者函数的输入参数中
2. 协变只能出现在方法或者函数的输出参数中
3. 不可变可以出现在任何的输入或者输出参数
 
所以参数是逆变的(消费者),返回值是协变的(生产者),这就是PECS(producers extends, consumers super)的来由,Scala的Function1[-T, +R]就是一个例子,特别说明一下Function1也可以写作f: A => B
 
trait Friend[-T] {
  def befriend(some: T): Unit = {
    ???
  }
}
class Person extends Friend[Person]
class Student extends Person
def friends(students: Array[Student], f: Student => Person) = {
  for (student <- students) yield f(_)
}

def findStudent(person: Person): Student = ???
这里的重点是friends方法的第二个函数参数,和findStudent的定义,正好是相反的
f: Student => Person
findStudent方法则是 Person => Student,Person是Student的父类,但是通过-T逆变成了子类,为什么?
因为f: Student => Person这个函数也是需要多态的缘故,又为什么?因为要扩大这个函数的适用范围。
 
具体分析一下:有函数参数定义为f: Student => Person,根据LSP原则进行多态的话,那么传入实际的函数值g如下几个Pattern:
g: Student => Person,和定义完全一样,没有问题
g: Person => Student,此时g的入参是Person,f的入参是Student,因为任何子类出现的位置都可以被替换为父类,那么f的入参当然可以带入到g的对应位置,再来看看子类,g的出参是Student,f的出参是Person,同理根据LSP,g的出参可以被替换成父类Person。
 
如果f和g的定义颠倒一下,我们再来看看会发生什么情况:f:Person => Studentg:Student => Person很显然,此时要将g带入到f的位置是不可能的,f的入参无法从Person转换成Student带入到g中,g的出参Person也无法转换成Student带入到f的出参中,所以这也就是PECS为什么这么定义的原因。

其他的几个Pattern都可以用同样的方法进行分析得出正确的结果:高阶函数的入参需要适应逆变,而出参需要适应协变。

posted @ 2015-10-24 21:53  thinking!!!  阅读(851)  评论(0编辑  收藏  举报