注:本文是我这段时间学习Scala类型系统时的一些所得,可能有很多地方不严谨或者不正确,欢迎指出。在些期间,得到了老猪、大魔头、教主等多位群友,以及无敌的stackoverflow的帮助,特此感谢!
在«Scala in programming»这本书中,关于协变、逆变这一章,藏着一只非常奇特的猫,我叫它“来自星星的猫”。
下面是原书内容,见“Scala编程”第19章“参数类型化”第258页:
下面是个显得有点生编硬造的例子,我们考虑如下的类型定义,其中若干位置的变化型被标了+(正的)或-(负的):
abstract class Cat[-T, +U] { def meow[W-](volue: T-, listener: Cat[U+, T-]-) :Cat[Cat[U+,T-]-, U+]+ }
类型参数W,以及两个值参数,volume和listener的位置都是负的。注意meow的结果类型,第一个Cat[U,T]参数的位置是负的,因为Cat的第一个类型参数T被标了-号。这个参数中的类型U重新转为正的位置(两次翻转),而参数中的类型T仍然是负的位置。
从讨论中你可以发现想要跟上变化型位置的变化是非常困难的。因此Scala编译器自动帮你完成这项工作,从而减轻了你的负担。
计算变化型的过程中,编译器检查每个类型参数是否仅用于分类正确的位置。这个例子中,T仅用在负的位置上,而U仅用于正的位置上。因此Cat类是类型正确的。
本文的目的,就是要看懂这只猫。
在一个类(class/triat)及方法中,有一些位置可以声明或使用类型,比如,以这只简单的猫为例:
trait Cat[A] {
def meow[B](volume: C): D
}
如上所示,共有4个位置是跟类型相关。其中A、B处可以声明类型,C、D处可以使用类型。
有一些位置有正负之分,比如B、C处是负的,D是正的。还有一些是中立的,在这里没有体现出来。
在这里需要强调一下:如果下文里提到“A位置”、“B位置”、“C位置”、“D位置”,如果上下文不顺畅,那么就是指这里的这四个位置。
书上提到了类型位置的正负,但是没有给它一个准确的定义,只是大概说了一下:“负”的位置只能用逆变,“正”的位置只能用“协变”。我查阅了很多资料,也问了很多人,都没有得到满意的答案。
以下是我自己的理解,可能对也可能不对,请小心阅读:
“类型”是我们的程序与编译器交流沟通的工具,它就像一些神秘的指令,我们利用它向编译器告诉我们的意图,编译器会试着理解它,生成更高效的代码,同时找出可能存在错误或矛盾的地方。
我们的“与类型相关的意图”反映在代码层面(类、方法),可以分为这三种:
这三种意图互相独立,并且当类型不是“无变”时,对类型的变化型还有不同的要求。我认为,如果一个位置是为了“对外提供”而声明和使用类型的,则是“正”的;如果是为了“处理”而声明或使用的,则为“负”的。“正”与“负”只是为了对两种意图进行区分和表达的。
这样D处就是正的,C处就是负的。而B处声明的类型通常用于C处,所以它也是负的。而A处声明的参数,并不确定是用在哪个地方,所以它的正负需要看具体情况而定。
我们知道,在scala中有三种变化型,分别是无变(invariant),协变(covariant),和逆变(contravariant)。
其中无变的情况比较特殊。如果一个参数被声明为无变,则它可以无视类型位置的正负,出现在任何地方。
而“协变”和“逆变”的目的在于,用于让编译检查:当泛型对象被看作它原本类型的父类型时,它的原有功能(“向外提供”或“处理外界传入数据”)的类型要求是否被破坏。
当以一个协变或逆变的类型作为上下界来声明一个新类型时,需要注意,新类型本身是“无变”的。比如:
trait Cat[+A] {
def meow[B >: A](x: B): B
}
trait Cat[-A] {
def meow[B <: A](x: B): B
}
上面的代码是可以通过编译的。这里的定义出来的B
类型本身并不是逆变的,它实际上是“无变”的,只是它依赖于另一个协变或者逆变的A
。
而无变的类型可以出现在任何位置,所以它可以放在参数和返回值位置。
再来个例子:
trait Cat[-A, T>:A] {
def meow(x: T): T
}
这里通过下界声明了一个类型T
,它可以同时用于方法参数和返回值处,说明它也是无变的。
如果我们给它加上变化型:
trait Cat[-A, +T>:A] {
def meow(x: T): T // !!! can't compile
}
它会报错说T
是协变的,不能出现在参数位置那里(因为那里是负位置)
上面那只奇特的猫,实际上是很复杂的。它成功的把很多问题和难点,以夸张的形式组合在了一起,让人很难一下子看懂。所以我们要减化问题,从易到难,一点点剖析它。
先以这只简化的猫为例:
trait Cat[-C, +D] {
def meow(volume: C): D
}
我们要回答两个问题:
再次强调,本文的讨论都是基于各类型是协变或者逆变的前提,不考虑“无变”的情况。
在下面更加简化的代码里,我们定义了一个协变的类型T
,它被用于返回值。
trait Cat[+T] {
def meow: T
}
它是可以通过编译的。但是如果T
是逆变的,就无法通过编译:
trait Cat[-T] {
def meow:T // !! can't be compiled
}
会提示错误:
error: contravariant type T occurs in covariant position in type => T of method meow
def meow:T
^
这是为什么呢?
首先看这只协变猫Cat[+T]
:
trait Cat[+T] {
def meow: T
}
val child = new Cat[String] {
def meow:String = "miaomiao~~"
}
val parent: Cat[Any] = child
由于T是协变的,所以Cat[String]
是Cat[Any]
的子类型,所以上面的赋值是成立的。
然后取返回值:
val voice:Any = parent.meow
由于parent的类型是Cat[Any]
,所以在编译器看来,parent.meow
返回值是Any
。同时不要忘了,parent
指向的是child
实例,所以voice
的值实际上是child.meow
返回的miaomiao~~
这个String。
由于String
实例总是Any
类型的,所以不会有任何编译错误。
那么是只逆变猫Cat[-T]
呢?
trait Cat[-T] {
def meow: T
}
根据逆变的定义,我们可以写出下面的赋值:
val child = new Cat[Any] {
def meow:Any = 123
}
val parent: Cat[String] = child
然后对返回值赋值:
val voice:String = parent.meow // error !!!
问题出来了。
由于parent
是Cat[String]
类型,所以我们可以期望parent.meow
的返回值也是String类型。但是不要忘了,parent
指向的是child
这个实例,而它的meow
返回值是123这个Integer,显然不能赋给String。
编译器帮我们检查到了错误,所以这段代码不能通过编译。
综合起来就是,方法的返回值必须是协变的,其位置是正的。
再来定义两只带参数的猫。
trait Cat[+T] {
def meow(voice: T) // !!! can't compile
}
trait Cat[-T] {
def meow(voice: T)
}
第一只是协变的,第二只是逆变的,为了简单,只定义了参数部分。其中前者是无法通过编译的,后者没有问题。
如果这是一只协变猫,让我们跟前面一样,来个转型赋值:
trait Cat[+T] {
def meow(voice: T)
}
val child = new Cat[String] {
def meow(voice: String) { println(voice.toLowerCase) }
}
val parent: Cat[Any] = child
根据协变的定义,上面的赋值是正确的。
然后传参数:
parent.meow(123) // error !!!
由于parent的类型是Cat[Any]
,所以它的meow
方法的类型是这样的:
def meow(voice: Any)
因为参数类型是Any,所以我们可以向它传入一个Integer。但是不要忘了,parent引用的实际上是child,而child的meow
只能接受String
类型,所以这个调用会报错。
编译器帮我们检查到了这个错误,所以这段代码不能编译。
如果这是一只逆变猫,根据逆变的定义,我们可以写出下面的赋值:
trait Cat[-T] {
def meow(voice: T)
}
val child = new Cat[Any] {
def meow(voice: Any) { println(voice) }
}
val parent: Cat[String] = child
由于parent
的类型是Cat[String]
,所以它对应的meow
方法的类型是这样的:
def meow(voice: String)
所以我们可以传个String进去:
parent.meow("miaomiao~~")
会不会出问题呢?由于parent引用的实际上是child对象,而child.meow
方法可以授受一个Any
类型的参数,所以这个调用是没有任何问题的。
综上所述,参数类型必须是逆变的,参数类型位置是负的。
这里要说的是:
trait Cat[A] {
def meow[B](volume: C): D
}
中的B位置,即我们在一个单独的方法上声明的类型。由于在这里定义的类型通常是给参数使用的,所以这个位置是负的。
一个负位置,可以接受“逆变”和“无变”的类型。在这里,我们没有办法声明一个“逆变”的类型,下面的代码是不能通过编译的:
trait Cat[A] {
def meow[-B]
}
会报语法错误。那么我们只能声明“无变”类型的。
最简单的方式是声明一个简单类型,比如
trait Cat[+A] {
def meow[B](x:B)
}
这里的B
跟协变参数A
之间没有任何关系,不管Cat对象如何向上转型,meow
都可以接受任意类型的参数。
实际上这种定义没多少用,因为B可以是任意类型,所以我们在meow
里,只能把它当作Any
来看待。所以更多的情况是以A作为上界或下界,对新类型进行限定。
由于A可能是协变、也可能是逆变,而B可能以A为上界、也可能以A为下界,所以就会有下面四种情况:
trait Cat1[+A] {
def meow[B >: A](x: B)
}
trait Cat2[+A] {
def meow[B <: A](x: B) // can't compile
}
trait Cat3[-A] {
def meow[B >: A](x: B) // can't compile
}
trait Cat4[-A] {
def meow[B <: A](x: B)
}
上面这四种情况,只有Cat1
和Cat4
是可以通过编译的,而Cat2
和Cat3
不能。为了解释这一点,我们需要知道三点:
后面两点后面会讨论,这里先知道这个结果就可以了。
结合这三点,我们就可以得出结论,只有Cat1
与Cat4
是符合要求的。
我们以Cat3
为例,看看违反以后,会出现什么问题。
trait Cat3[-A] {
def meow[B >: A](x: B) // can't compile
}
val child:Cat3[Any] = new Cat3[Any] {
def meow[B >: Any](x: B) = x
}
val parent: Cat3[String] = child
然后我们调用:
parent.meow("miaomiao~~")
这时会出现错误,因为parent
实际上指向的是child
,所以我们向child.meow()
传入了一个String类型的参数。而child的meow的类型是:
def meow[B >: Any](x: B) = x
意思是说,“我只能处理Any类型的父类”,但是传过来的String是Any的子类,无法处理。
先看这只逆变猫:
trait Cat[-A] {
def meow(a: A)
}
当我们把参数类型指明为A
时,我们隐藏的意思是说:meow
可以接受A的任意子类型。
与下面代码等价:
trait Cat[-A] {
def meow[B <: A](a: B)
}
这段代码可以这么理解:如果我有一个对象是Cat[Any]
,那么它的meow
实际处理能力是所有Any的子类
。同时因为A
是逆变的,所以它可以看作是Cat[String]
的子类,那么展示出来的meow
就变成了meow[B:<String]
,宣称自己只能够处理所有String的子类
。由于宣称的处理能力比实际处理能力弱,所以可以轻松处理传入的各String子类对象,不会有任何问题。
如果A是协变的,即:
trait Cat[+A] {
def meow[B >: A](a: B)
}
此时对于给定的类型A
,meow
只能处理它的父类型。如果我有一个对象是Cat[String]
,那么meow
的实际处理能力就是“所有String的父类型”。同时因为A是协变的,所以它可以看作是Cat[Any]
的子类,所以宣称的处理能力就变成了“所有Any的父类型”。由于宣称的处理能力比实际处理能力弱,所以也不会有问题。
对于负位置来说,它定义的类型是用于参数,让方法“处理输入”的。而下界符号>:
会把类型的处理能力由默认的“处理子类”变成“处理父类”,这两个正好是相反的,所以会翻转位置的正负。
所以,如果某个位置[X >: Y]
是“正”的,那么Y
所在的位置就是“负”的,需要传入一个逆变的Y。如果某个位置[X >: Y]
是“负”的,那么Y所在位置就是正的,需要传入一个协变的Y。
如果我们的猫的meow参数或者返回值,是一个拥有协变或逆变参数的泛型呢?
定义一只老鼠先:
trait Mouse[+T]
再来一只吃老鼠的猫:
trait Cat[-A] {
def eat(mouse: Mouse[A])
}
这段代码是可以通过编译的。需要注意的是,Mouse[+T]
中的T
是协变的,其位置为正,但是在eat
里,它却可以接受一个逆变类型A。
让我们试一下:
val child: Cat[Any] = new Cat[Any] {
def eat(mouse: Mouse[Any]) = mouse
}
val parent: Cat[String] = child
然后传一只老鼠进去:
parent.eat(new Mouse[String])
由于Mouse[+T]
是协变的,所以我们传入的Mouse[String]
是Mouse[Any]
的子类,满足了child.eat(Mouse[Any])
的类型要求。
但是如果我们的猫是协变的:
trait Cat[+A] {
def eat(mouse: Mouse[A])
}
val child: Cat[String] = new Cat[String] {
def eat(mouse: Mouse[String]) = mouse
}
val parent: Cat[Any] = child
由于parent的eat是:
def eat(mouse: Mouse[Any])
且Mouse是协变的,那么我们可以传一只Mouse[Int]
进去:
parent.eat(new Mouse[Int])
消化不良。。。
同样的道理,我们还可以验证当Mouse是逆变,以及放在返回值位置的情况。
我们可以从这个例子中感受一下,但是很难证明对于更复杂的情况(如嵌套类型Mouse[Mouse[Mouse[T]]]
)也一样,但是好在scala规范中已经明确告诉了我们结论:
即:
最后再来看这只猫:
abstract class Cat[-T, +U] { def meow[W-](volue: T-, listener: Cat[U+, T-]-) :Cat[Cat[U+,T-]-, U+]+ }
现在,是否可以看懂了?
“一个泛型拥有的变化型位置的正负,跟它所在的变化型位置正好相反"这是怎么理解的?
这句话是翻译的,我觉得这样写可能更准确(如果没有理解错误的话):一个泛型如果处于一个负的位置,那么它拥有的变化型位置要反转(即正变负,或负变正);如果处于一个正的位置,则不变。可参考文章开始引用的那个Cat的例子理解
我处理逆变逆变一般是先用+A,如果编译器报错就改成-A,如果再报错就改成_ : A,再报错就用普通的A。