您的位置:首页 > 其它

在Scala中,为什么函数的参数类型是逆变的,而函数的返回值协变的

2017-03-20 01:01 337 查看

在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
4000
=> 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


所以函数的返回类型是协变的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  scala 逆变 协变
相关文章推荐