当前位置: 首页 > Scala, 程序代码, 随记 > 正文

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],YX子类,因为参数+B是协变的,则NodeYNodeX子类.

可按下述步骤理解 函数的参数类型是逆变的,而返回类型是协变的。 :

  1. 你有一个方法 f(p:NodeX),且在f方法体中调用了p.prepend(elemX:X),注意这里的elemX是X类型的.
  2. 对于方法f,因为NodeY是NodeX的子类型,那么调用f的时候,传递一个q:NodeY作为参数必定是可以的(因为上述重要原则).
  3. 当真的传递一个q:NodeY时,在f方法体内,发生调用p.prepend(elem)的地方,实际发生的调用情况是q.prepend(elemX),注意参数依然是elemX啊,而q.prepend需要的是一个Y类型的elemY参数,是X的子类
  4. 在需要子类参数的地方,传父类显然是不行的,与上述重要原则相悖

上面四步分析之后可以发现开头的那个例子中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的父类.

为什么修改成这样,就能满足上述重要原则呢? 还是按照下面的步骤来分析:

  1. 你有一个方法 f(p:NodeX),且在f方法体中调用了p.prepend(elemE:E),注意这里的elemE是E类型的,E是X的父类(根据类型下界限制prepend [U >: B]).
  2. 对于方法f,因为NodeY是NodeX的子类型,那么调用f的时候,传递一个q:NodeY作为必定是可以的(因为上述重要原则),X是Y的父类.
  3. 当真的传递一个q:NodeY时,在f方法体内,发生调用q.prepend()的地方,实际发生的调用情况是q.prepend(elemE:E),注意参数依然是elemE啊,假设q.prepend(elemF)需要的是一个F类型的elemF参数,F是Y的父类,可以是X本身或者X的父类E,而elemE正好是E类型的,是X的父类,满足上述重要原则.
  4. 上述继承(父子)关系为 E <- X <- F <- Y (F也可以就是X本身)
赞 赏

   微信赞赏  支付宝赞赏


本文固定链接: https://www.jack-yin.com/coding/3183.html | 边城网事

该日志由 边城网事 于2020年07月01日发表在 Scala, 程序代码, 随记 分类下, 你可以发表评论,并在保留原文地址及作者的情况下引用到你的网站或博客。
原创文章转载请注明: Scala语言备忘拾遗 – 3 类型边界 | 边城网事
【上一篇】
【下一篇】

Scala语言备忘拾遗 – 3 类型边界 暂无评论

发表评论

快捷键:Ctrl+Enter