Scala语言备忘拾遗 – 3 类型边界
Scala语言备忘拾遗 – 3 类型边界
Scala中加上类型边界约束之后,泛型类的类型参数没有那么
泛
了.
1. 类型上界
类型上界
将泛型类中的类型参数限制为某个类的子类. 参考 这里
符号 P <: Pet 将类型参数P限制为 Pet 的子类,比如 Cat 或 Dog .
看下面的例子
abstract class Animal {
def name: String
}
abstract class Pet extends Animal {}
class Cat extends Pet {
override def name: String = "Cat"
}
class Dog extends Pet {
override def name: String = "Dog"
}
class Lion extends Animal {
override def name: String = "Lion"
}
class PetContainer[P <: Pet](p: P) {
def pet: P = p
}
val dogContainer = new PetContainer[Dog](new Dog)
val catContainer = new PetContainer[Cat](new Cat)
类PetContainer接受一个必须是Pet子类的类型参数P。因为Dog和Cat都是Pet的子类,所以可以构造PetContainer[Dog]和PetContainer[Cat]。
// this would not compile
val lionContainer = new PetContainer[Lion](new Lion)
在尝试构造PetContainer[Lion]的时候会得到下面的错误信息:
type arguments [Lion] do not conform to class PetContainer’s type parameter bounds [P <: Pet]
这是因为Lion并不是Pet的子类。
2. 类型下界
类型下界 将类型声明为另一种类型的超类型。 术语 B >: A 表示类型参数 B 是类型 A 的超类型。
在大多数情况下,A 将是类的类型参数,而 B 将是方法的类型参数。 参考 这里
看一个例子:
trait Node[+B] {
def prepend(elem: B): Node[B]
}
如果在IDE中敲出上面这段代码,会得到下面的错误信息,编译不能通过:
Covariant type B occurs in contravariant position in type B of value elem
意思就是 协变类型参数B(注意, 参数B前面有个 + ), 出现在逆变点 -- 方法 prepend 中的参数 elem 是逆变的。
概括的说,就是 函数的参数类型是逆变的,而返回类型是协变的。
- 协变 — 对于泛型类
Node[T]
,假设有 Note[X],Note[Y],如果 Y是X的子类,那么Note[Y]也是Note[X]的子类 - 逆变 — 与协变相反,对于泛型类
Node[T]
,假设有 Note[X],Note[Y],如果 Y是X的子类,那么Note[Y]是Note[X]的父
类
更多协变
和逆变
,参考 这里
那么,函数的参数类型是逆变的,而返回类型是协变的。
这句话怎么理解呢? 下面仅分析这句话的前半部分
要理解这句话,需要先了解一个重要原则:
假如一个函数F(P:T),其参数P的类型是T, 那么该函数F接受类型为T及T的任意子类作为参数.
简单的说,在Scala中凡是接受类型T实例的地方,也必须接受类型T的子类的实例.
比如一个方法接受一个Animal参数,方法体中会print出Animal的name.而Cat是Animal的子类,则传递Cat给这个方法,在方法中print出Cat的name必是可行的.
毕竟Cat继承自Animal,会有Animal的公开的属性和方法.
函数的参数类型是逆变的,而返回类型是协变的。
这个原则正是为了让泛型类及其子类也能满足上面的重要原则.
回到类型下界
开头的例子,假如有两个具体的Node, NodeX:Node[X]
,以及NodeY:Node[Y]
,Y
是X
的子类
,因为参数+B是协变
的,则NodeY
是NodeX
的子类
.
可按下述步骤理解 函数的参数类型是逆变的,而返回类型是协变的。
:
- 你有一个方法 f(p:NodeX),且在f方法体中调用了
p.prepend(elemX:X)
,注意这里的elemX是X类型的. - 对于方法f,因为NodeY是NodeX的子类型,那么调用f的时候,传递一个
q:NodeY
作为参数必定是可以的(因为上述重要原则). - 当真的传递一个
q:NodeY
时,在f方法体内,发生调用p.prepend(elem)
的地方,实际发生的调用情况是q.prepend(elemX)
,注意参数依然是elemX啊
,而q.prepend需要的是一个Y类型的elemY参数,是X的子类 - 在需要子类参数的地方,传父类显然是不行的,与上述重要原则相悖
上面四步分析之后可以发现开头的那个例子中trait Node[+B]
的定义是有问题的,然而编译器已经看穿了一切,早早的就报错了’Covariant type B occurs in contravariant position in type B of value elem’.
要解决这个问题,我们需要将方法 prepend 的参数 elem 的型变翻转。 通过引入一个新的类型参数 U ,该参数具有 B 作为类型下界,用符号表示为 U >: B, 限制U必须为B父类.
trait Node[+B] {
def prepend[U >: B](elem: U): Node[U]
}
修改成上述代码后, prepend接受的参数U必须是B的父类.
为什么修改成这样,就能满足上述重要原则呢? 还是按照下面的步骤来分析:
- 你有一个方法 f(p:NodeX),且在f方法体中调用了
p.prepend(elemE:E)
,注意这里的elemE是E类型的,E是X的父类(根据类型下界限制prepend[U >: B]
). - 对于方法f,因为NodeY是NodeX的子类型,那么调用f的时候,传递一个
q:NodeY
作为必定是可以的(因为上述重要原则),X是Y的父类. - 当真的传递一个
q:NodeY
时,在f方法体内,发生调用q.prepend()
的地方,实际发生的调用情况是q.prepend(elemE:E)
,注意参数依然是elemE啊
,假设q.prepend(elemF)需要的是一个F类型的elemF参数,F是Y的父类,可以是X本身或者X的父类E,而elemE正好是E类型的,是X的父类,满足上述重要原则. - 上述继承(父子)关系为 E <- X <- F <- Y (F也可以就是X本身)
微信赞赏 支付宝赞赏