在Scala中,为什么函数的参数类型是逆变的,而函数的返回值协变的
概念一
首先,需要明确一点的就是Liskov替换原则。以一段java代码为例,如果一个方法的参数它的类型是C,那么在调用这个方法的时候,
class C {
public void m() {
System.out.println("m");
}
}
class CSub extends C {
@Override
public void m() {
System.out.println("m sub");
}
}
public class Liskov {
public void f(C c) {
c.m();
}
public static void main(String[] args) {
// 传入C
Liskov liskov = new Liskov();
liskov.f(new C());
// 传入C的子类
liskov.f(new CSub());
}
}
概念二:逆变协变
在声明Scala的泛型类型时,“+”表示协变,而“-”表示逆变。
C[+T]:如果A是B的子类,那么C[A]是C[B]的子类。
C[-T]:如果A是B的子类,那么C[B]是C[A]的子类。
我们先定义三层的类型继承结构
class CSuper { def msuper() = println("CSuper")}
class C extends CSuper { def m() = println("C") }
class CSub extends C { def msub() = println("CSub") }
逆变示例
先自定义一个FunctionX的trait,他的T类型是逆变的
scala> trait FunctionX[-T]
在使用这个trait的时候,对于常量x的定义要求是FunctionX[C],那么根据逆变的定义,FunctionX[C]和FunctionX[CSuper]的对象是可以赋值给FunctionX[C],但是FunctionX[CSub]却不可以。
scala> val x: FunctionX[C] = new FunctionX[CSuper]{}
x: FunctionX[C] = $anon$1@bbd4791
scala> val x: FunctionX[C] = new FunctionX[C]{}
x: FunctionX[C] = $anon$1@15f35bc3
scala> val x: FunctionX[C] = new FunctionX[CSub]{}
<console>:15: error: type mismatch;
found : FunctionX[CSub]
required: FunctionX[C]
val x: FunctionX[C] = new FunctionX[CSub]{}
^
协变示例
先自定义一个FunctionY的trait,他的T类型是协变的
scala> trait FunctionY[+T]
在使用这个trait的时候,对于常量y的定义要求是FunctionY[C],那么根据协变的定义,FunctionY[C]和FunctionY[CSub]的对象是可以赋值给FunctionY[C],但是FunctionY[CSuper]却不可以。
scala> val y: FunctionY[C] = new FunctionY[CSub]{}
y: FunctionY[C] = $anon$1@11f23203
scala> val y: FunctionY[C] = new FunctionY[C]{}
y: FunctionY[C] = $anon$1@4b87760e
scala> val y: FunctionY[C] = new FunctionY[CSuper]{}
<console>:14: error: type mismatch;
found : FunctionY[CSuper]
required: FunctionY[C]
val y: FunctionY[C] = new FunctionY[CSuper]{}
^
逆变协变示例
为了结合逆变和协变自定义一个FunctionZ的trait,它的T类型是逆变,R类型是协变
scala> trait FunctionZ[-T, +R]
结合上面的逆变和协变的示例,可以很好的理解该trait的使用示例
scala> val z: FunctionZ[C, C] = new FunctionZ[CSuper, CSub]{}
z: FunctionZ[C,C] = $anon$1@4afd65fd
scala> val z: FunctionZ[C, C] = new FunctionZ[C, C]{}
z: FunctionZ[C,C] = $anon$1@5563bb40
scala> val z: FunctionZ[C, C] = new FunctionZ[CSub, CSuper]{}
<console>:15: error: type mismatch;
found : FunctionZ[CSub,CSuper]
required: FunctionZ[C,C]
val z: FunctionZ[C, C] = new FunctionZ[CSub, CSuper]{}
^
概念三: 函数字面量
在Scala中,匿名函数也称为函数字面量。例如:
scala> List(1,2,3,4).map(i => i + 3)
res5: List[Int] = List(4, 5, 6, 7)
函数表达式i => i + 3实际上是一个语法糖,编译器会将其转化为scala.Function1的匿名子类,其实现如下:
scala> val f: Int => Int = new Function1[Int, Int] {
| def apply(i: Int): Int = i + 3
| }
f: Int => Int = <function1>
scala> List(1,2,3,4).map(f)
res6: List[Int] = List(4, 5, 6, 7)
当定义了f,我们就可以指定参数列表调用它,其实他就会调用默认的apply函数。
在这个示例中,当List调用map方法的时候,List中的每一个元素都会被传递给f,如f(1)。实际上f(1)是f.apply(1)。
FunctionN是抽象的,因为其中的apply方法是抽象方法。当我们使用更简洁的代码i => i + 3 时,编译器为我们定义了apply方法。匿名函数的函数体就是用来定义apply的。
trait Function
在Scala中,函数其实也是对象,它是scala.Function0 -> Scala.Function22的对象。既然是对象,那么它就可以有不同的实现。
在这些trait中,定义了apply方法,apply接受的参数就是函数的参数,而apply的返回值就是函数的返回值。
首先来看一个接受一个参数的函数的泛型定义。
trait Function1[-T1, +R] {
def apply(v1: T1): R
}
其中参数类型为泛型类型T1,返回类型为泛型类型R。那么在实际定义函数的时候T1和R的类型会被确定下来,只不过需要注意的是,T1的前面有一个“-”,而R的前面有一个“+”
结合Scala中函数其实是FunctionN的对象以及之前FunctionX、FunctionY、FunctionZ的逆变协变示例
假如这个时候需要一个函数f他的定义如下
var f: C => C = (c: C) => new C //1.
// ↓ ↓
// ↓逆变 协变↓
// ↓ ↓
f = (c: CSuper) => new CSub //2.
f = (c: CSub) => new CSuper //3.
由于Function1的参数类型是逆变,返回类型是协变,所以前两种方式都是都可以的(函数也是对象,既然是对象,那么它就可以有不同的实现。),第三种编译就报错了。
但是注意函数f的定义它是: C => C,那么我们在使用这个函数的时候,要求我们传入的是C类型的对象,返回的也是C类型的对象。
参数类型
我们先看一下参数类型,函数的定义要求传入的参数是C类型,那么可以传入的对象是C及其子类的对象,函数的第一种很好理解
第二种要求的是CSuper,我们在调用函数的时候传入的肯定是C及其子类的对象,这些对象也肯定是CSuper类型的,想象一下,我们在第二种函数体中调用了CSuper的msuper()方法,那么C及其子类的对象也可以调用这个方法,但是第三种实现是不行的。
为什么第三种不行,假如在调用这个函数的时候传入的参数是C的对象,根据函数的定义这个是没有问题的。但是函数的实现中要求的是CSub(及其子类)的对象,显然是无法满足要求的。假如第三种实现的参数可以为CSub类型,那么我们可以在第三种函数实现的函数体中调用CSub的msub()方法,这个时候如果我们传入的是C的对象,而它并没有msub这个方法。
所以,函数的参数类型是逆变的。
返回类型
再看返回类型,函数的定义要求返回值C类型,既然返回的是C的对象,那么我就可以调用C的m()方法
如果返回值是CSuper的对象,它并没有m()方法,也就是说返回值不管是C还是C的子类都能当做C类型来使用
scala> val f1: C => C = (c: C) => new C
f1: C => C = <function1>
scala> val r: C = f1(new C)
r: C = C@3d0035d2
scala> r.m
C
scala> val f2: C => C = (c: CSuper) => new CSub
f2: C => C = <function1>
scala> val r: C = f2(new C)
r: C = CSub@1df1ced0
scala> r.m
C
所以函数的返回类型是协变的。