1. 类型边界

类型边界是与类型相关的规则,一个变量要匹配一个类型时必须符合这些规则。

类型边界的两种形式:

  • 类型上界(超类型约束,也称为一致性关系)
  • 类型下界(子类型约束)

类型上界是指,某一类型必须是另一种类型的子类型。类型下界表示某类型必须是另一个类型的父类(或该类型本身)。

类型边界与型变标记是两个不相干的问题。类型边界对参数化类型所允许采用的类型做了限制,如T <: AnyRef。型变标记表示参数化类型的子类实例是否可以替换父类实例。

实际场景中,常常使用型变标记和类型边界配合的工作方式,这主要是为了解决在错误的位置使用型变参数的问题,下面以Option的getOrElse方法作为例子进行解释:

sealed abstract class Option[+A] extends Product with Serializable {
    ...
    @inline final def getOrElse[B >: A](default: => B): B = {...}
    ...
}

可以看到,为何getOrElse方法返回B(A的父类型)呢?这里解释原因。


In [1]:
class Parent(val value: Int) { 
    override def toString = s"${this.getClass.getName}($value)"
}

class Child(value: Int) extends Parent(value)


defined class Parent
defined class Child

In [2]:
val op1: Option[Parent] = Option(new Child(1))
val p1: Parent = op1.getOrElse(new Parent(10))


op1: Option[Parent] = Some(cmd0$$user$Child(1))
p1: Parent = cmd0$$user$Child(1)

In [3]:
val op2: Option[Parent] = Option[Parent](null) // None
val p2a: Parent = op2.getOrElse(new Parent(10)) // Result: Parent(10)
val p2b: Parent = op2.getOrElse(new Child(100)) // Result: Child(100)


op2: Option[Parent] = None
p2a: Parent = cmd0$$user$Parent(10)
p2b: Parent = cmd0$$user$Child(100)

In [4]:
val op3: Option[Parent] = Option[Child](null) // None
val p3a: Parent = op3.getOrElse(new Parent(20)) // Result: Parent(20)
val p3b: Parent = op3.getOrElse(new Child(200)) // Result: Child(200)


op3: Option[Parent] = None
p3a: Parent = cmd0$$user$Parent(20)
p3b: Parent = cmd0$$user$Child(200)

关键在这里:

val op3: Option[Parent] = Option[Child](null)
val p3a: Parent = op3.getOrElse(new Parent(20))

op3显式地将Option[Child](null)(即None)赋给了Option[Parent]

但从调用者的角度,我们并不知道真实类型到底是什么?如果调用者持有对Option[Parent]的引用,那么将自然认为它可以从Option[Parent]中提取一个Parent值。故如果是None的话,调用者将返回默认的Parent参数;如果是Some[Parent],则返回Some中的值。所有情况都认为返回一个Parent类型的值。但实际返回的是Child子类的实例。如果不适用类型下界说明,那么val p3a: Parent = op3.getOrElse(new Parent(20))语句将无法通过类型检查。

这就是编译器不允许简单的方法签名,而采用[B >: A]边界标记的签名的原因。

同时使用类型上下界的例子


In [5]:
class Upper
class Middle1 extends Upper
class Middle2 extends Middle1
class Lower extends Middle2
case class C[A >: Lower <: Upper](a: A)
// case class C2[A <: Upper >: Lower](a: A) // Does not compile


defined class Upper
defined class Middle1
defined class Middle2
defined class Lower
defined class C

2. 通过引入新的类型参数来解决协变和逆变故障

这里的实例中,我们实现一个List中简化的++版本,将两个集合类型组合起来。

我们希望能有自动转换功能,比如把字符串列表转换为Any列表,所以把参数类型标注为协变。


In [6]:
// ++方法定义为接受另一个ItemType类型的里诶包作为参数
// 返回新列表
trait List[+ItemType] {
    def ++(other: List[ItemType]): List[ItemType]
}


Compilation Failed
Main.scala:78: covariant type ItemType occurs in contravariant position in type $user.this.List[ItemType] of value other

    def ++(other: List[ItemType]): List[ItemType]
           ^

上面由于ItemType出现在了逆变位置上,出现了编译报错。

为了绕开编译器限制,我们可以用新类型参数来避免把ItemType放在逆变位置上。


In [7]:
// 简单绕开型变约束
trait List[+ItemType] {
    def ++[OtherItemType](other: List[OtherItemType]): List[ItemType]
}


defined trait List

In [8]:
// 实现空List类
class EmptyList[ItemType] extends List[ItemType] {
    def ++[OtherItemType](other: List[OtherItemType]) = other
}


Compilation Failed
Main.scala:81: type mismatch;
 found   : cmd6.this.$ref$cmd5.List[OtherItemType]
 required: cmd6.this.$ref$cmd5.List[ItemType]
    def ++[OtherItemType](other: List[OtherItemType]) = other
                                                        ^

由于上面定义的方法得到的结果类型不匹配,OtherItemType和ItemType类型不兼容,造成编译失败。

可以通过对OtherItemType做某种类型约束,使得OtherItemType和ItemType类型建立联系。

我们希望OtherItemType是能和当前列表很好的组合的类型,因为ItemType是协变的,那么可以把当前列表向ItemType层级上方转换。因此,我们用ItemType作为OtherItemType的下界约束,我们修正++方法,返回OtherItemType类型。


In [9]:
trait List[+ItemType] {
    def ++[OtherItemType >: ItemType](
        other: List[OtherItemType]): List[OtherItemType]
}


defined trait List

In [10]:
class EmptyList[ItemType] extends List[ItemType] {
    def ++[OtherItemType >: ItemType](
        other: List[OtherItemType]) = other
}


defined class EmptyList

// 确认把各类型的空list组合是否返回我们期望的类型


In [11]:
val strings = new EmptyList[String]


strings: EmptyList[String] = cmd7$$user$EmptyList@fb8491

In [12]:
val ints = new EmptyList[Int]


ints: EmptyList[Int] = cmd7$$user$EmptyList@1cc1a6

In [13]:
val anys = new EmptyList[Any]


anys: EmptyList[Any] = cmd7$$user$EmptyList@1b7cf4

In [14]:
strings ++ strings


res11: List[String] = cmd7$$user$EmptyList@fb8491

In [15]:
strings ++ ints


res12: List[Any] = cmd7$$user$EmptyList@1cc1a6

In [16]:
strings ++ anys


res13: List[Any] = cmd7$$user$EmptyList@1b7cf4

可以看到,编译器推断出Any是String和Int的共同超类,于是得到了Any列表,这正是我们期望的结果。

一般来说,当在类方法里碰到协变和逆变故障时,通常的解决办法是引入一个新的类型参数,在方法签名里用新引入的类型参数。

所以,当我们向一个不可变集合添加新元素以构成一个新的集合时(包括上面这个例子),其类型参数必须具有逆变的行为,但传入的是协变的参数化类型。

总的来说,那些类型参数为协变的参数化类型,与方法参数的类型下界关系密切。