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

在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

所以函数的返回类型是协变的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值