Scala笔记01 -- 继承、协变与逆变
下面为博客摘要,详细内容请看http://keke046.github.io/2021/11/12/scala-learn/01-covariance-contravariance/
能不能让一个类型既有协变、逆变的特性,又能够做为输入和输出呢?实际上是可以的。为什么说“继承”是模糊的呢?给一个例子。。`T`是逆变的,我们应该把它声明为:```scalaclass Printer[-T] { def print(x: T): Unit = 。。这时候,“继承”的概念是模糊的。也即是,模板类与参数的继承方向相同。。而`Contra[+T]`,随着`T`向父亲的方向走,`B:T`的范围变大了,变得像父亲了,所以是协变。 def pop(): Unit = 。96。96。像这样的,与模板的输出无关的类型`T`,就是不变的类型。这给我们造成了很多限制。** 模板类是类的函数。同时`Queue[Person]`也不能转换为`Queue[Hineven]`,因为后者可以`var a: Hineven = q。96。比如下面的代码,人员列表里装满了 [`Hineven`](http://47。96。而如果一个类本身是逆变的,这样的关系就会反过来:```scalaclass Test1[+T] { def foo(x: Contra[T]): Covar[T]}class Test2[-T] { def foo(x: Covar[T]): Contra[T] // 协变和逆变关系倒过来了}```而如果输入的是函数,输出的是函数,它们的参数就会再倒过来:```scalaclass Test1[+T] { def foo(x: (Covar[T])=Contra[T]): (Contra[T])=Covar[T]}class Test2[-T] { def foo(x: (Contra[T])=Covar[T]): (Covar[T])=Contra[T]}```# 类型成员的协变和逆变```scalaclass Covar[-T] { type A : T}class Contra[+T] { type B : T}```首先,`Convar`和`Contra`都限定了一个范围。 def pop_front(): Unit = 。}```第三种情况,是通用类型`queue`:```scalaclass Queue[T] { def top: T = 。draw()def debug(a: Programmer): Unit = a。191。`A`有`B`的接口,完全可以看做`A`继承了`B`,因为B能做的,A都能做。有些时候,我们希望模板类生成的东西,能够具有与它输入相关的继承关系。1/hineven/) ,我们希望它可以转换成`Programmer`类型的消息列表,然后喂给一个其它的函数。e能做的,B。。那么对于A的任意一个成员e,A。。# Magic Stack刚刚说道,一个类型既作为输入,又作为输入,它似乎就只能是不变的。asInstance[Artist]```为什么 [`Hineven`](http://47。# 继承:一种包含关系类的关键特征,是它的继承关系。 作为输入类型的类型是逆变的 作为输出类型的类型是协变的这个断言非常本质。。1/hineven/) 都能做。# 逆变两遍这可能吗?构造一个trivil的协变类型`Covar[+T]`,和逆变类型`Contra[-T]`:```scalaclass Covar[+T]class Contra[-T]```我们知道了函数的返回值是协变的,输入是逆变的。```scalanew Hineven()。“模板”其实就是代码重用,是面向对象里面一个重要的概念:模板类可以看做是类的模板,类可以看做是实例的模板,而特征(trait)就是更高级的模板了。这里引入了一个问题:为什么类和它的成员,类型的协变、逆变、不变必须要相同呢?其实这个很容易在逻辑上证明(口头证明)。* 接口和特质能减少代码的混乱,它们像是一种程序语法上的处理而不是编译原理上的处理。让我们抛开继承、特质和接口,专注这样**你能做的,我都能做**的包含关系上来。注意大范围能做的,小范围都能做,所以小范围是儿子。。96。e都能做。96。也即是,模板类与参数的继承方向相反。191。而`Queue`,`top`函数要求`T`是协变的,而`push`函数要求它是逆变的,于是`T`只能是不变的。}var hinven_printer = new Printer[Hineven]var person_printer = new Printer[Person]```这时,按照逻辑来说,应该是 `hinven_printer` 能做的 `person_printer` 都能做。这样,`Printer`和它的输入`T`的继承关系是反相关的。但在scala或java中,类除了可以继承,还可以有接口(或特质)。(下面的 $A:B$ 指的 $A$ 继承 $B$ ) $A:B \Rightarrow F[A] : F[B]$ 的,就是协变。那么我们可以把逆变的函数重写,让它创造一个新的类:```scalaclass Stack[+A] { self = def push[B : A](x: B): Stack[B] = new Stack[B] { override def top: B = x override def pop: Stack[A] = Stack。e具有与A,B相同的协变与逆变关系。# 输入输出和函数你可能已经感受到了,协变和逆变与模块的输入和输出有密切的关系。# 协变和逆变:一种单调性模板类实际上像是一个类的函数,输入一些类,然后构造一个新的类。这才是继承的本质。1/hineven/) 可以当 `Artist` 来用呢?因为`Artist`能做的 [`Hineven`](http://47。 def push(x: T): Unit = 。继承从来不应该是一棵树,而应该是集合的包含图。。。1/hineven/) 名义上继承了 `Person`,但在我们的逻辑上(至少脑海里),已经在把它当`Artist`和`Programmer`来用了。!-- indicate-the-source --**协变和逆变的关键在与“变”字。。}```这时候`Queue[Hineven]`不能转换为`Queue[Person]`,因为后者可以`push(new Person())`而前者不可以。而且,我们甚至可以将其强制转换为它的接口类型。这样的情况下,我们更希望它是协变的。。。观察`Convar[-T]`,随着`T`向父亲的方向走,`A:T`的范围变小了,变得像儿子了,这样逆变。。下面的代码声明了一个[`Hineven`](http://47。 A能做的,B都能做。}var a = new PersonList[Hineven]def start_debug_all(list: PersonList[Programmer]) = 。回顾`Printer`里面,`T`作为输入,只需要是逆变就可以了。实际上,我个人觉得,这些接口、特质等等,和多重继承没有太大的区别。。 $A:B \Rightarrow F[A] : F[B]$ 的,就是逆变。注意`Person`类是 [`Hineven`](http://47。于是A。所以`T`是协变的(不是`PersonList`),我们应该把它声明为:```scalaclass PersonList[+T] { // blablabla}```而当我们需要一个打印机的时候,情况就会反过来:```scalaclass Printer[T] { def print(x: T): Unit = 。。toString() // 也可以用scala的语法糖,self在第一行声明了 } def top: A = throw new StackOverflowError def pop: Stack[A] = throw new StackOverflowError override def toString() = ""}```这样,逆变的部分就被绕过了。1/hineven/),他既是艺术家也是程序员:```scalatrait Artist trait Programmer class Hineven extends Person with Artist with Programmer```!-- more --而在写其它的的文件的时候,可能会这样写:```scaladef draw(a: Artist): Unit = a。其实,在scala中,函数类`Function`是这样声明的:```scalatrait Function3[-T1, -T2, -T3, +R] { def apply(v1: T1, v2: T2, v3: T3): R}```可以看到,参数都是逆变的,返回值是协变的。。```scalaclass PersonList[T] { def get_front(): T = 。top` 而前者不可以。1/hineven/) 类的父亲,而`Printer[Person]`是`Printer[Hinven]`的儿子。this // 确保 this 引用的是外层的 Stack 而不是内层的 override def toString() = x。191。191。```这时候,`PersonList`和它的输入`T`的继承关系正相关。toString() + " " + self。e和B。191。协变和逆变,实际上像是函数的单调关系。191。模板类大多都是作为容器使用的,先构造好,然后交付给一个其它的库。但模板的功能依然没有改变。debug()```这时候,虽然 [`Hineven`](http://47。
协变和逆变的关键在与“变”字。 模板类是类的函数。协变和逆变,实际上像是函数的单调关系。(下面的 $A<:B$ 指的 $A$ 继承 $B$ )
$A<:B \Rightarrow F[A] <: F[B]$ 的,就是协变。也即是,模板类与参数的继承方向相同。
$A<:B \Rightarrow F[A] >: F[B]$ 的,就是逆变。也即是,模板类与参数的继承方向相反。
继承:一种包含关系
类的关键特征,是它的继承关系。但在scala或java中,类除了可以继承,还可以有接口(或特质)。这时候,“继承”的概念是模糊的。
为什么说“继承”是模糊的呢?给一个例子。下面的代码声明了一个Hineven
,他既是艺术家也是程序员:
1 | trait Artist |
能不能让一个类型既有协变、逆变的特性,又能够做为输入和输出呢?实际上是可以的。为什么说“继承”是模糊的呢?给一个例子。。`T`是逆变的,我们应该把它声明为:```scalaclass Printer[-T] { def print(x: T): Unit = 。。这时候,“继承”的概念是模糊的。也即是,模板类与参数的继承方向相同。。而`Contra[+T]`,随着`T`向父亲的方向走,`B:T`的范围变大了,变得像父亲了,所以是协变。 def pop(): Unit = 。96。96。像这样的,与模板的输出无关的类型`T`,就是不变的类型。这给我们造成了很多限制。** 模板类是类的函数。同时`Queue[Person]`也不能转换为`Queue[Hineven]`,因为后者可以`var a: Hineven = q。96。比如下面的代码,人员列表里装满了 [`Hineven`](http://47。96。而如果一个类本身是逆变的,这样的关系就会反过来:```scalaclass Test1[+T] { def foo(x: Contra[T]): Covar[T]}class Test2[-T] { def foo(x: Covar[T]): Contra[T] // 协变和逆变关系倒过来了}```而如果输入的是函数,输出的是函数,它们的参数就会再倒过来:```scalaclass Test1[+T] { def foo(x: (Covar[T])=Contra[T]): (Contra[T])=Covar[T]}class Test2[-T] { def foo(x: (Contra[T])=Covar[T]): (Covar[T])=Contra[T]}```# 类型成员的协变和逆变```scalaclass Covar[-T] { type A : T}class Contra[+T] { type B : T}```首先,`Convar`和`Contra`都限定了一个范围。 def pop_front(): Unit = 。}```第三种情况,是通用类型`queue`:```scalaclass Queue[T] { def top: T = 。draw()def debug(a: Programmer): Unit = a。191。`A`有`B`的接口,完全可以看做`A`继承了`B`,因为B能做的,A都能做。有些时候,我们希望模板类生成的东西,能够具有与它输入相关的继承关系。1/hineven/) ,我们希望它可以转换成`Programmer`类型的消息列表,然后喂给一个其它的函数。e能做的,B。。那么对于A的任意一个成员e,A。。# Magic Stack刚刚说道,一个类型既作为输入,又作为输入,它似乎就只能是不变的。asInstance[Artist]```为什么 [`Hineven`](http://47。# 继承:一种包含关系类的关键特征,是它的继承关系。 作为输入类型的类型是逆变的 作为输出类型的类型是协变的这个断言非常本质。。1/hineven/) 都能做。# 逆变两遍这可能吗?构造一个trivil的协变类型`Covar[+T]`,和逆变类型`Contra[-T]`:```scalaclass Covar[+T]class Contra[-T]```我们知道了函数的返回值是协变的,输入是逆变的。```scalanew Hineven()。“模板”其实就是代码重用,是面向对象里面一个重要的概念:模板类可以看做是类的模板,类可以看做是实例的模板,而特征(trait)就是更高级的模板了。这里引入了一个问题:为什么类和它的成员,类型的协变、逆变、不变必须要相同呢?其实这个很容易在逻辑上证明(口头证明)。* 接口和特质能减少代码的混乱,它们像是一种程序语法上的处理而不是编译原理上的处理。让我们抛开继承、特质和接口,专注这样**你能做的,我都能做**的包含关系上来。注意大范围能做的,小范围都能做,所以小范围是儿子。。96。e都能做。96。也即是,模板类与参数的继承方向相反。191。而`Queue`,`top`函数要求`T`是协变的,而`push`函数要求它是逆变的,于是`T`只能是不变的。}var hinven_printer = new Printer[Hineven]var person_printer = new Printer[Person]```这时,按照逻辑来说,应该是 `hinven_printer` 能做的 `person_printer` 都能做。这样,`Printer`和它的输入`T`的继承关系是反相关的。但在scala或java中,类除了可以继承,还可以有接口(或特质)。(下面的 $A:B$ 指的 $A$ 继承 $B$ ) $A:B \Rightarrow F[A] : F[B]$ 的,就是协变。那么我们可以把逆变的函数重写,让它创造一个新的类:```scalaclass Stack[+A] { self = def push[B : A](x: B): Stack[B] = new Stack[B] { override def top: B = x override def pop: Stack[A] = Stack。e具有与A,B相同的协变与逆变关系。# 输入输出和函数你可能已经感受到了,协变和逆变与模块的输入和输出有密切的关系。# 协变和逆变:一种单调性模板类实际上像是一个类的函数,输入一些类,然后构造一个新的类。这才是继承的本质。1/hineven/) 可以当 `Artist` 来用呢?因为`Artist`能做的 [`Hineven`](http://47。 def push(x: T): Unit = 。继承从来不应该是一棵树,而应该是集合的包含图。。。1/hineven/) 名义上继承了 `Person`,但在我们的逻辑上(至少脑海里),已经在把它当`Artist`和`Programmer`来用了。!-- indicate-the-source --**协变和逆变的关键在与“变”字。。}```这时候`Queue[Hineven]`不能转换为`Queue[Person]`,因为后者可以`push(new Person())`而前者不可以。而且,我们甚至可以将其强制转换为它的接口类型。这样的情况下,我们更希望它是协变的。。。观察`Convar[-T]`,随着`T`向父亲的方向走,`A:T`的范围变小了,变得像儿子了,这样逆变。。下面的代码声明了一个[`Hineven`](http://47。 A能做的,B都能做。}var a = new PersonList[Hineven]def start_debug_all(list: PersonList[Programmer]) = 。回顾`Printer`里面,`T`作为输入,只需要是逆变就可以了。实际上,我个人觉得,这些接口、特质等等,和多重继承没有太大的区别。。 $A:B \Rightarrow F[A] : F[B]$ 的,就是逆变。注意`Person`类是 [`Hineven`](http://47。于是A。所以`T`是协变的(不是`PersonList`),我们应该把它声明为:```scalaclass PersonList[+T] { // blablabla}```而当我们需要一个打印机的时候,情况就会反过来:```scalaclass Printer[T] { def print(x: T): Unit = 。。toString() // 也可以用scala的语法糖,self在第一行声明了 } def top: A = throw new StackOverflowError def pop: Stack[A] = throw new StackOverflowError override def toString() = ""}```这样,逆变的部分就被绕过了。1/hineven/),他既是艺术家也是程序员:```scalatrait Artist trait Programmer class Hineven extends Person with Artist with Programmer```!-- more --而在写其它的的文件的时候,可能会这样写:```scaladef draw(a: Artist): Unit = a。其实,在scala中,函数类`Function`是这样声明的:```scalatrait Function3[-T1, -T2, -T3, +R] { def apply(v1: T1, v2: T2, v3: T3): R}```可以看到,参数都是逆变的,返回值是协变的。。```scalaclass PersonList[T] { def get_front(): T = 。top` 而前者不可以。1/hineven/) 类的父亲,而`Printer[Person]`是`Printer[Hinven]`的儿子。this // 确保 this 引用的是外层的 Stack 而不是内层的 override def toString() = x。191。191。```这时候,`PersonList`和它的输入`T`的继承关系正相关。toString() + " " + self。e和B。191。协变和逆变,实际上像是函数的单调关系。191。模板类大多都是作为容器使用的,先构造好,然后交付给一个其它的库。但模板的功能依然没有改变。debug()```这时候,虽然 [`Hineven`](http://47。