Scala是世界上最好的语言(一):Type Bound

这个语法应该是Scala最常用的语法之一。它比C#的类型约束走的更远,因为它既可以表示上界,也可以表示下界。不过在此之前需要搞清几个概念。

Variance Position

我们知道Java允许数组协变,这会导致将一个子类添加到父类数组时的异常:

String[] a1 = { "abc" };
Object[] a2 = a1;
a2[0] = new Integer(17);
String s = a1[0];
复制代码

从Scala一切皆方法的角度,就是数组的update方法的参数是一个类型参数。更一般的来看,这里update的参数是一个抗变点(contravariant position),或者说负向点(negative position)。其实我觉得负向点的说法更好,因为它可以与型变的概念区分开。一个抗变点不允许接受协变的类型参数。另外,还有2种型变点:协变点(covariant position),不变点(novariant position)。
由于类型参数本身也是一个类型,因此泛型是可以嵌套的。这给最终验证型变注解带来了一定的复杂性。在语言规范里是这样说明的:

The top-level of the type or template is always in covariant position. The variance position changes at the following constructs.

  • The variance position of a method parameter is the opposite of the variance position of the enclosing parameter clause.
  • The variance position of a type parameter is the opposite of the variance position of the enclosing type parameter clause.
  • The variance position of the lower bound of a type declaration or type parameter is the opposite of the variance position of the type declaration or parameter.

这里型变发生了翻转(flipped)。一个函数参数的型变点会翻转,同时类型参数(如果有下界或者型变注解)的型变点也会翻转。这是 Programming in Scala 中构造的示例:

abstract class Cat[-T, +U] {
def meow[W−](volume: T−, listener: Cat[U+, T−]−)
: Cat[Cat[U+, T−]−, U+]+ }
复制代码

注意到这里Cat[U, T]的U和T互换了位置,因为他们所在的型变点发生了翻转。比如,对于返回值而言,因为最外层的Cat的第一个类型参数是抗变的,所以U翻转为协变点,T为抗变点(规则2)。同样listener也发生了翻转(规则1)。
原理上,方法的参数是逆变点,而返回值是协变点。这主要是基于里氏替换原则的推理。举个例子:

abstract class Box[+F <: Fruit] {
  def fruit: F
  def contains(aFruit: Fruit) = fruit.name.equals(aFruit.name)
}
class AppleBox(apple: Apple) extends Box[Apple] {
  def fruit = apple
}
var fruitBox: Box[Fruit] = new AppleBox(new Apple)
var fruit: Fruit = fruitBox.fruit
复制代码

这里的F是典型的协变应用。不过,假设这里F是抗变的,那么就会出现问题,因为作为父类的AppleBox需要返回一个apple,而子类fruitBox并不能替换它。
最后,Scala默认的行为是不变(novariant)。这也是一个更符合逻辑的抉择。

Lower Bound

下界既可以是一个具体的类,也可以是一个类型参数。还是用 Programming in Scala 的例子,它通过下界允许对一个协变类调用包含逆变点的方法:

class Queue[+T] (private val leading: List[T], private val trailing: List[T] ) {
    ...
    def append[U >: T](x: U) = new Queue[U](leading, x :: trailing)
}
复制代码

这里可以将一个Orange追加到Queue[Apple]上得到一个Queue[Fruit]。注意到当作为Queue[Fruit]时,U必须是Fruit的超类,因此这个下界防止了前面Java代码中的赋值错误。
更一般的来说,这里的下界定义了一次翻转(规则3)。这其实很好理解:我们总是在父类中调用方法append,而T是U的子类。
Programming in Scala 在这里提到了术语声明处型变(declaration-site variance)与使用处型变(use-site variance)。这里的site是声明型变的位置。Scala是声明处型变的,这一点和C#相同。Java是使用处型变的,这种风格要求程序员必须很清楚类的可变性,缺少了编译器的支持。

Reference

[1], Programming in Scala, 3rd, Martin Odersky, Lex Spoon, Bill Benners
[2], Scala Language Specification, scala-lang.org/files/archi…
[3], blog.codecentric.de/en/2015/04/…

猜你喜欢

转载自juejin.im/post/5bcc6ac1e51d457aa12dc9a6
今日推荐