在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
所以函数的返回类型是协变的。
相关文章推荐
- Programming In Scala笔记-第十九章、类型参数,协变逆变,上界下界
- scala学习笔记-类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用
- Scala入门到精通——第二十一节 类型参数(三)-协变与逆变
- Scala类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用
- Scala类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用
- Programming In Scala笔记-第十九章、类型参数,协变逆变,上界下界
- Scala类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用
- Scala类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用
- Scala入门到精通——第二十一节 类型参数(三)-协变与逆变
- C#为什么支持协变的参数只能用于方法的返回值?支持逆变的参数只能用于方法参数?
- Scala 深入浅出实战经典 第81讲:Scala中List的构造是的类型约束逆变、协变、下界详解
- _stdcall 调用类型 函数参数压栈方式为什么是从右到左的?
- 为什么WinMain函数返回类型是int PASCAL?
- C++基础::语法特性::函数重写(override)与协变返回类型(covariant return type)
- Scala深入浅出进阶经典 第81讲:Scala中List的构造是的类型约束逆变、协变、下界详解
- Scala 深入浅出实战经典 第81讲:Scala中List的构造是的类型约束逆变、协变、下界详解
- [译]委托和接口泛型参数类型的协变和逆变
- C为什么要把数组类型的函数参数认为是指向数组第一个元素的指针
- 让boolean类型验证用户函数同时修改返回输入的参数对象
- C/C++—— 写一个函数,它的参数为指向函数的指针,返回类型也为指向函数的指针