类型边界是与类型相关的规则,一个变量要匹配一个类型时必须符合这些规则。
类型边界的两种形式:
类型上界是指,某一类型必须是另一种类型的子类型。类型下界表示某类型必须是另一个类型的父类(或该类型本身)。
类型边界与型变标记是两个不相干的问题。类型边界对参数化类型所允许采用的类型做了限制,如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的父类型)呢?这里解释原因。
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
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)
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)
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]边界标记的签名的原因。
同时使用类型上下界的例子
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
这里的实例中,我们实现一个List中简化的++版本,将两个集合类型组合起来。
我们希望能有自动转换功能,比如把字符串列表转换为Any列表,所以把参数类型标注为协变。
// ++方法定义为接受另一个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放在逆变位置上。
// 简单绕开型变约束
trait List[+ItemType] {
def ++[OtherItemType](other: List[OtherItemType]): List[ItemType]
}
defined trait List
// 实现空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类型。
trait List[+ItemType] {
def ++[OtherItemType >: ItemType](
other: List[OtherItemType]): List[OtherItemType]
}
defined trait List
class EmptyList[ItemType] extends List[ItemType] {
def ++[OtherItemType >: ItemType](
other: List[OtherItemType]) = other
}
defined class EmptyList
// 确认把各类型的空list组合是否返回我们期望的类型
val strings = new EmptyList[String]
strings: EmptyList[String] = cmd7$$user$EmptyList@fb8491
val ints = new EmptyList[Int]
ints: EmptyList[Int] = cmd7$$user$EmptyList@1cc1a6
val anys = new EmptyList[Any]
anys: EmptyList[Any] = cmd7$$user$EmptyList@1b7cf4
strings ++ strings
res11: List[String] = cmd7$$user$EmptyList@fb8491
strings ++ ints
res12: List[Any] = cmd7$$user$EmptyList@1cc1a6
strings ++ anys
res13: List[Any] = cmd7$$user$EmptyList@1b7cf4
可以看到,编译器推断出Any是String和Int的共同超类,于是得到了Any列表,这正是我们期望的结果。
一般来说,当在类方法里碰到协变和逆变故障时,通常的解决办法是引入一个新的类型参数,在方法签名里用新引入的类型参数。
所以,当我们向一个不可变集合添加新元素以构成一个新的集合时(包括上面这个例子),其类型参数必须具有逆变的行为,但传入的是协变的参数化类型。
总的来说,那些类型参数为协变的参数化类型,与方法参数的类型下界关系密切。