快学Scala第17章----类型参数

本章要点

  • 类、特质、方法和函数都可以有类型参数
  • 将类型参数放置在名称之后,以方括号括起来。
  • 类型界定的语法为 T <: UpperBound、 T >: LowerBound、 T <% ViewBound、 T : ContextBound
  • 你可以用类型约束来约束另一个方法,比如(implicit ev: T <:< UpperBound)
  • 用+T(协变)来表示某个泛型类的子类型关系和参数T方向一致, 或用-T(逆变)来表示方向相反。
  • 协变适用于表示输出的类型参数,比如不可变集合中的元素
  • 逆变适用于表示输入的类型参数,比如函数参数

泛型类

和Java或C++一样,类和特质可以带类型参数。在Scala中,使用方括号来定义类型参数:

class Pair[T, S] (val first: T, val second: S)

带有一个或多个类型参数的类是泛型的。


泛型函数

def getMiddle[T](a: Array[T]) = a(a.length / 2)

类型变量界定

有时, 你需要对类型变量进行限制。

class Pair[T](val first: T, val second: T) {
  def smaller = if (first.compareTo(second) < 0) first else second   //  error
}

这是错误的,我们并不知道first是否有compareTo方法。要解决这个问题,我们可以添加一个上界 T <: Comparable[T] :

class Pair[T <: Comparable[T]](val first: T, val second: T) {
  def smaller = if (first.compareTo(second) < 0) first else second
}

这里T必须是Comparable[T]的子类型。这样我们就可以实例化Pair[java.lang.String],但是不能实例化Pair[java.io.File] 。
你可以为了类型指定一个下界。例如:

class Pair[T] (val first: T, val second: T) {
  def peplaceFirst[newFirst: T] = new Pair[T] (newFirst, second)
}

假定我们有一个Pair[Student],我们应该允许使用一个Person来替换第一个组件。这样做的结果将会是一个Pair[Person]。通常而言,替换进来的类型必须是原类型的超类型。

def replaceFirst[R >: T] (newFirst: R) = new Pair[R](newFirst, second)
// 或者
def replaceFirst[R >: T] (newFirst: R) = new Pair(newFirst, second)  // 类型推导

**注意: **如果不写下界:

def replaceFirst[R] (newFirst: R) = new Pair(newFirst, second)

该方法可以编译通过,但是它将返回Pair[Any].


视图界定

在面前上界时我们提供了一个示例:

class Pair[T <: Comparable[T]]

但是如果你new一个Pair(4,2), 编译器会抱怨说Int不是Comparable[Int]的子类型。因为Scala的Int类型没有实现Comparable,不过RichInt实现了Comparable[Int],同时还有一个从Int到RichInt的隐式转换。
解决方法是使用“视图界定”:

class Pair[T <% Comparable[T]]

<% 意味着T可以被隐式转换成Comparable[T]。
**说明: **用Ordered特质会更好,它在Comparable的基础上额外提供了关系操作符:

class Pair[T <% Ordered[T]] (val first: T, val second: T) {
  def smaller = if( first < second ) first else second
}

上下文界定

视图界定 T <% V 要求必须存在一个从T到V的隐式转换,上下文界定的形式为 T:M,其中M是另一个泛型类。它要求必须存在一个类型为M[T]的“隐式值”。例如:

class Pair[T: Ordering]

上述定义要求必须存在一个类型为Ordering[T]的隐式值。当你声明一个使用隐式值的方法时,你需要添加一个“隐式参数”。例如:

class Pair[T: Ordering]  (val first: T, val second: T) {
  def smaller(implicit ord: Ordering[T]) = {
    if( ord.compare(first, second) < 0) first else second
  }
}

Manifest上下文界定

要实例化一个泛型的Array[T],我们需要一个Manifest[T]对象。 如果你要编写一个泛型函数来构造泛型数组的话,你需要传入这个Manifest对象来帮忙。由于它是构造器的隐式参数,可以用上下文界定:

def makePair[T: Manifest](first: T,  second: T) {
   val r = new Array[T](2)
   r(0) = first
   r(1) = second
   r
}

多重界定

类型变量可以同时有上界和下界。写法为:

T >: Lower <: Upper

你不能同时有多个上界或多个下界。不过你可以要求一个类型实现多个特质:

T <: Comparable[T] with Serializable with Cloneable

你也可以有多个视图界定:

T <% Comparable[T] <% String

你也可以有多个上下文界定:

T : Ordering : Manifest

类型约束

类型约束提供给你的是另一个限定类型的方式。总共有三种关系可供使用:

T =:= U   // T是否等于U
T <:< U   // T是否为U的子类型
T <%< U  // T能否被视图(隐士)转换为U

要使用这样一个约束,你需要添加“隐式类型证明参数”:

class Pair[T](val first: T, val second: T) (implicit ev: T <:< Comparable[T])

类型约束让你可以在泛型类中定义只能在特定条件下使用的方法。例如:

class Pair[T] (val first: T, val second: T) {
  def smaller(implicit ev: T <:< Ordered[T]) = {
    if (first < second) first else second
  }
}

在这里你可以构造出Pair[File], 尽管File并不是带有先后次序的。只有当你调用smaller方法时,才会报错。
另一个示例是Option类的orNull方法:

val friends = Map("Fred" -> "Barney", ...)
val friendOpt = friends.get("Wilma")
val friendOrNull = friendOpt.orNull  // 要么是String,要么是null

这种做法并不适用于值类型,例如Int。因为orNUll实现带有约束Null <:< A, 你仍然可以实例化Option[Int],只要你别使用orNull就好了。

类型约束的另一个用途是改进类型推断。例如:

def firstLast[A, C <: Iterable[A]](it: C) = (it.head, it.last)

当你执行如下代码:

firstLast(List(1,2,3))

你会得到一个消息,推断出的类型参数[Nothing, List[Int]]不符合[A, C <: Iterable[A]]。类型推断器单凭List(1,2,3)无法判断出A是什么,因为它在同一个步骤中匹配到A和C。解决的方法是首先匹配C,然后在匹配A:

def firstLast[A, C] (it: C) (implicit ev: C <:< Iterable[A]) = (it.head, it.last)

型变

假定我们有一个函数对pair[Person]做某种处理:

def makeFriends(p: Pair[Person])

如果Student是Person的子类,那么可以用Pair[Student]作为参数调用吗?缺省情况下,这是个错误。尽管Student是Person的子类型,但Pair[Student]和Pair[Person]之间没有任何关系。
如果你想要这样的关系,则必须在定义Pair类时表明这一点:

class Pair[+T] (val first: T, val second: T)

+号意味着该类型是与T协变的-----也就是说,它与T按痛样的方向型变。由于Student是Person的子类,Pair[Student]也就是Pair[Person]的子类型。

也可以有另一个方向的型变--逆变。例如:泛型Friend[T],表示希望与类型T的人成为朋友的人。

trait Friend[-T] {
  def befriend(someone: T)
}

// 有这么一个函数
def makeFriendWith(s: Student, f: Friend[Student]) { f.befriend(s) }

class Person extends Friend[Person]
class Student extends Person
val s = new Student
val p = new Person

函数调用makeFriendWith(s, p)能成功吗?这是可以的。
这里类型变化 的方向和子类型方向是相反的。Student是Person的子类,但是Friend[Student]是Friend[Person]的超类。这就是逆变。

在一个泛型的类型声明中,你可以同时使用这两中型变,例如 单参数函数的类型为Function1[-A, +R]

def friends(students: Array[Atudent], find: Function1[Student, Person]) = {
  for (s <- students) yield find(s)
}

协变和逆变点

从上面可以看出函数在参数上是逆变的,在返回值上则是协变的。通常而言,对于某个对象消费的值适用逆变,而对于产出它的值则适用于协变。 如果一个对象同时是消费和产出值,则类型应该保持不变,这通常适用于可变数据结构。
如果试着声明一个协变的可变对偶,则是错误的,例如:

class Pair[+T] (var first: T, var second: T)  // 错误

不过有时这也会妨碍我们做一些本来没有风险的事情。例如:

class Pair[+T] (var first: T, var second: T) {
  def replaceFirst(newFirst: T) = new Pair[T](newFirst, second)  // error  T出现在了逆变点
}

// 解决方法是给方法加上另一个类型参数:
class Pair[+T] (var first: T, var second: T) {
  def replaceFirst[R >: T](newFirst: R) = new Pair[R](newFirst, second)
}

对象不能泛型

你不能给对象添加类型参数。


类型通配符

在Java中,所有泛型类都是不变的。不过,你可以在使用通配符改变它们的类型。例如:

void makeFriends(Pair<? extends Person> people)   // Java代码

可以用List<Student>作为参数调用。

你也可以在Scala中使用通配符

def process(people: java.util.List[_ <: Person])  // scala

在Scala中,对于协变的Pair类,不需要通配符。但是假定Pair是不变的:

class Pair[T] (var first: T, var second: T)

// 可以定义函数:
def makeFriends (p: Pair[_ <: Person])  // 可以用Pair[Student]调用

逆变使用通配符:

import java.util.Comparator
def min[T](p: Pair[T]) (comp: Comparator[_ >: T])
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 变量初始化可以用用 _ 作占位符,赋值为默认值,字符串 null,Float、Int、Double 等为 0var...
    FaDeo_O阅读 897评论 0 0
  • object 变量可指向任何类的实例,这让你能够创建可对任何数据类型进程处理的类。然而,这种方法存在几个严重的问题...
    CarlDonitz阅读 906评论 0 5
  • 大数据学院_腾讯大数据http://data.qq.com/academySpark是一个通用的并行计算框架,立足...
    葡萄喃喃呓语阅读 575评论 0 1
  • 室内全彩显示屏都有哪些? 室内led大屏幕一般分为: 直插型全彩LED显示屏,一般为户外和半户外屏;一种为表面贴装...
    易事达LED显示屏阅读 796评论 0 0
  • 勤工这学期最后一次例会开完了呢,论我为何一直毫无怨言死心踏地的一直跟着我们大部长,人好心灵各种俏,在这里对我们的陈...
    真性情才最吸引人阅读 230评论 0 0