scala学习笔记 - 类型参数(二)

25 篇文章 0 订阅
15 篇文章 2 订阅

多重界定

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

T >: Lower <: Upper

不能同时有多个上界或多个下界;不过,你依然可以要求一个类型实现多个特质,就像这样:

T <: Comparable[T] with Serializable with Cloneable

可以有多个上下文界定:

T : Ordering : ClassTag

类型约束

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

T =:= U // T是否等于U
T <:< U // 是否为U的子类型
T => U // 能否被转换为U

这些约束将会测试T是否等于U,是否为U的子类型,或能否被转换为U,要使用这样一个约束,你需要添加“隐式类型证明参数(implicit evidence parameter)”,就像这样:

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

在前面的示例中,使用类型约束并没有带来比类型界定class Pair[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[URL],尽管URL并不是带先后次序的,只有当你调用smaller方法时,才会报错。
另一个示例是Option类的orNull方法:

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

在和Java代码互相调用时,orNull方法就很有用了,因为Java中通常习惯用null表示缺少某值,不过这种做法并不适用于值类型,比如Int,它们并不把null看作合法的值,因为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]]不符合[C <: Iterable[A]]。为什么是Nothing?类型推断器单凭List (1, 2, 3)无法判断出A是什么,因为它是在同一个步骤中匹配到A和C的,要帮它解决这个问题,首先匹配C,然后匹配A:

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

说明:corresponds方法检查两个序列是否有相互对应的条目:

def corresponds[B](that: Seq[B])(match: (A, B) => Boolean): Boolean

match前提是一个柯里化的参数,因此类型推断器可以首先判定类型B,然后用这个信息来分析match。在如下调用中:

Array("Hello", "Fred").corresponds(Array(5, 4))(_.length == _)

编译器能推断出Int,从而理解_.length == _是怎么一回事。

型变

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

def makeFriends(p: Pair[Person])

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

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

+号意味着该类型是与T协变(covariant)的,也就是说,它与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) }

你能用Friend[Person]作为参数调用它吗?也就是说,如果你有:

class Person extends Friend[Person]{ ... } 
class Student extends Person 
val susan = new Student 
val fred = new Person

函数调用makeFriendWith(susan, fred)能成功吗?看上去应该成功才是,如果Fred愿意和任何人成为朋友,一定也会想要成为susan的朋友。
注意类型变化的方向和子类型方向是相反的。Student是Person的子类型,但Friend[Student]是Friend[Person]的超类型,对于这种情况,你需要将类型参数声明为逆变( contravariant )的:

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

在一个泛型的类型声明中,你可以同时使用这两种型变。举例来说,单参数函数的类型为Function1[-A, +R],要搞明白为什么这样的声明是正确的,考虑如下函数:

def friends(students: Array[Student], find: Function1[Student, Person]) = 
    // 你可以将第二个参数写成find: Student => Person 
    for (s <- students) yield find(s)

假定你有一个函数:

def findStudent(p: Person): Student

你能用这个函数调用friends吗?当然可以。它愿意接受任何Person,因此当然也愿意接受Student,它将产出Student结果,该结果可以被放入Array[Person]。

协变和逆变点

函数在参数上是逆变的,在返回值上则是协变的。通常而言,对于某个对象消费的值适用逆变,而对于它产出的值则适用协变。
如果一个对象同时消费和产出某值,则类型应该保持不变( invariant )。这通常适用于可变数据结构,举例来说,在Scala中的数组是不支持型变的,你不能将一个Array[Student]转换成Array[Person],反过来也不行,这样做会不安全,考虑如下情形:

val students = new Array[Student](length) 
val people: Array[Person] = students // 非法,但假定我们可以这样......
people(O) = new Person("Fred") // 现在students(O)不再是Student了
// 反过来讲,
val people = new Array[Person](length) 
val students: Array[Student] = people // 非法,但假定我们可以这样做.....
people(O) = new Person("Fred") // 现在students(0)不再是Student了

说明:在Java中,我们可以将Student[]数组转换为Person[]数组,但如果你试着把非Student类的对象添加到该数组时,就会抛出ArrayStoreException。在Scala中,编译器会拒绝可能引发类型错误的程序通过编译。
假定我们试过声明一个协变的可变对偶,会发现这是行不通的,它会是一个带有两个元素的数组,不过会报刚才你看到的那个错。的确,如果你用:

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

就会得到一个报错,说在如下的setter方法中,协变的类型T出现在了逆变点:

first_=(value: T)

参数位置是逆变点,而返回类型的位置是协变点。
不过,在函数参数中,型变是反转过来的,它的参数是协变的。比如下面Iterable[+A]的foldLeft方法:

foldLeft[B](z: B)(op: (B, A) => B): B 
                       -  +     + - +

注意A现在位于协变点。
这些规则很简单也很安全,不过有时它们也会妨碍我们做一些本来没有风险的事。不可变对偶的replaceFirst方法:

class Pair[+T](val first: T, val second: T) { 
  def replaceFirst (newFirst: T) = new Pair[T](newFirst, second) // 错误
}

编译器拒绝上述代码,因为类型T出现在了逆变点,但是,这个方法不可能会破坏原本的对偶,它返回新的对偶。解决方法是给方法加上另一个类型参数,就像这样:

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

这样一来,方法就成了带有另一个类型参数R的泛型方法,但R是不变的,因此出现在逆变点就不会有问题。

对象不能泛型

我们没法给对象添加类型参数;比如,可变列表。元素类型T的列表要么是空的,要么是一个头部类型为T 、尾部类型 List[T]的节点:

abstract class List[+T] { 
	def isEmpty: Boolean 
	def head: T 
	def tail: List[T]
}
class Node[T](val head: T, val tail: List[T]) extends List[T] { 
	def isEmpty = false
}
class Empty[T] extends List[T] { 
	def isEmpty = true 
	def head = throw new UnsupportedOperationException 
	def tail = throw new UnsupportedOperationException
}

说明:这里用Node和Empty是为了让讨论对Java程序员而言比较容易。如果你对Scala列表很熟悉的话,只要在脑海中将其替换成::和Nil即可。
将Empty定义成类看上去有些傻,因为它没有状态,但是,你又无法简单地将它变成对象:

object Empty[T] extends List[T] // 错误

你不能将参数化的类型添加到对象,在本例中,解决方法是继承List[Nothing]:

object Empty extends List[Nothing]

Nothing类型是所有类型的子类型,因此,当我们构造如下单元素列表时,
val lst= new Node(42, Empty )
类型检查是成功的。根据协变的规则,List[Nothing]可以被转换成List[Int],因而Node[Int]的构造器能够被调用。

类型通配符

在Java中,所有泛型类都是不变的,不过,你可以在使用时用通配符改变它们的类型,举例来说,方法

void makeFriends (List<? extends Person> people)

可以用List<Student>作为参数调用。
你也可以在Scala中使用通配符,它们看上去是这个样子的:

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

在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])

类型通配符是用来指代存在类型的“语法糖”。
注意:在某些特定的复杂情形下,Scala的类型通配符还并不是很完善。举例来说,如下声明在Scala2.12中行不通:

def min[T <: Comparable[ _ >: T]](p: Pair[T]) = ...

解决方法如下:

type SuperComparable[T] = Comparable[ _ >: T] 
def min[T <: SuperComparable[T]] (p: Pair[T]) = ...

参考:快学scala(第二版)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值