实际场景中,常常使用型变标记和类型边界配合的工作方式,这主要是为了解决在错误的位置使用型变参数的问题,下面以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)
In [2]:
val op1: Option[Parent] = Option(new Child(1))
val p1: Parent = op1.getOrElse(new Parent(10))
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)
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)
关键在这里:
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
In [6]:
// ++方法定义为接受另一个ItemType类型的里诶包作为参数
// 返回新列表
trait List[+ItemType] {
def ++(other: List[ItemType]): List[ItemType]
}
上面由于ItemType出现在了逆变位置上,出现了编译报错。
为了绕开编译器限制,我们可以用新类型参数来避免把ItemType放在逆变位置上。
In [7]:
// 简单绕开型变约束
trait List[+ItemType] {
def ++[OtherItemType](other: List[OtherItemType]): List[ItemType]
}
In [8]:
// 实现空List类
class EmptyList[ItemType] extends 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]
}
In [10]:
class EmptyList[ItemType] extends List[ItemType] {
def ++[OtherItemType >: ItemType](
other: List[OtherItemType]) = other
}
// 确认把各类型的空list组合是否返回我们期望的类型
In [11]:
val strings = new EmptyList[String]
In [12]:
val ints = new EmptyList[Int]
In [13]:
val anys = new EmptyList[Any]
In [14]:
strings ++ strings
In [15]:
strings ++ ints
In [16]:
strings ++ anys
可以看到,编译器推断出Any是String和Int的共同超类,于是得到了Any列表,这正是我们期望的结果。
一般来说,当在类方法里碰到协变和逆变故障时,通常的解决办法是引入一个新的类型参数,在方法签名里用新引入的类型参数。
所以,当我们向一个不可变集合添加新元素以构成一个新的集合时(包括上面这个例子),其类型参数必须具有逆变的行为,但传入的是协变的参数化类型。
总的来说,那些类型参数为协变的参数化类型,与方法参数的类型下界关系密切。