模式匹配是 Scala 的重要组成部分,可以将它认为是 Java 的 switch-case 语句的泛化版本。在实际的 Scala 应用中,利用模式匹配和递归的组合可以写出高度抽象的逻辑。除了模式匹配本身以外,这里还会涉及样例类 ( 也称作模板类 ),提取器两个重要概念。
1. 基本用法
Scala 的模式匹配使用 match 关键字声明,每个分支使用 case 关键字,然后 使用=> 符号衔接语句块。和 Java 的 switch 语句有所不同,每个 case 之间不会贯穿。在执行匹配时,程序会从第一个分支开始尝试匹配,并仅执行第一个匹配成功的代码块。
换句话说, case 的声明顺序会影响程序的运行结果。因此,越 "精准,具体" 的 case 优先级应该越高,或者说 "写得越上面"。下面是一个简单的模式匹配实例:
val a = 10
val b = 20
var operator = '+'
var result = 0
operator match {
//case [condition] => {Block}
case '+' => result = a + b
case '-' => result = a - b
case '*' => result = a * b
case '/' if b !=0 => result = a / b
case _ =>
print("invalid operator.")
result = -1
}
println(s"result = $result")
这段代码是模式匹配最最基本的用法 ( 相当于是 switch 语句 ),当匹配字面量时,程序要做的仅仅是将 operator 和这些字面量做值比较。
注意,如果所有 case 分支都不满足条件,那么就会程序抛出 ErrorMatch 异常,因为 Scala 认为这是程序设计者因设计不考虑周全而引发的 "bug" 。
有时为了避免异常抛出,我们常常在模式匹配的末尾加上 case _ ( 如上述代码块那样 ),这是一个 通配模式 。_ 符号在此处表示忽视对 operator 变量进行匹配,而直接运行对应的代码块。显然,这个匹配优先级应当是最低的,我们只将它用作 "在任何其它 case 都不生效" 的备用情况,可类比路由器转发表中优先级最低的默认路由项。
1.1 插入条件守卫
除了 case 匹配之外,还可以利用条件守卫插入额外的判断,写法如 case ... if ... => ...。比如在下面的代码块中,给定一个值 value ,我们利用条件守卫来实现对 value 进行区间上的判断:
val value = 12
value match{
case _ if value>0 && value<50 => println("该值在0-50区间。")
case _ if value>=50 && value<100 => println("该值在50-100区间。")
case _ if value>=100 => println("该值在100及以上。")
}
只有在同时满足 case 和 if 的条件时,程序才会运行对应的代码块。另外,每个函数块可以用缩进形式来代替花括号,在保持代码可读性的同时也让代码变得更加整洁。
value match {
case 100 =>
println("每个分支的代码块可以不用括号括起来")
println("但是需要空行。")
case _ => println("如果要写在同一行");println("则语句之间需要用分号隔开。")
}
1.2 模式匹配具备返回值
Scala 的任何一个语句块都具备返回值 ( 即便是 Unit ),模式匹配也不例外:
val value = 1000
val result: Int = value match {
//该分支满足条件,因此这个模式匹配会返回1。
case 1000 => println("value = 1000.") 1
case _ => 0
}
if (result == 1) println("模式匹配成功")
具体的返回值取决于程序最终会选择哪个分支。同时,所有的 case 分支都应当返回相同类型的返回值,或者都返回 Unit 。
2. 类型匹配
case 关键字后面可以带上一个临时的不可变变量,这个临时变量只在当前分支内生效,比如下面代码块的 a 和 b :
val result = value match {
case a => println(a)
case b => println(b)
}
在检查第一个分支时,value 的值会传给 a ;在检查第二个分支时,value 值会传给 b 。当然,单纯的赋值并没有意义,我们需要将赋值操作和条件判断结合在一起使用,这里先介绍类型匹配的用法。
假设现在四个身份:Student,Teacher,Doctor,Anonymity,它们都继承 Person 类。现在的任务是利用模式匹配来检查一个上转型对象 person 的实际类型:
val person: Person = new Student
val value = person match {
//这些p变量互不冲突,因为它们只存在自己的作用域中。
case p: Student => println("他是一名学生。")
1
case p: Teacher => println("他是一名老师。")
2
case p: Doctor => println("他是一名医生。")
3
case _ => println("他是一名神秘客,应该是Anonymity类型。")
4
}
print(s"模式匹配返回结果:$value")
程序在每个分支内都做了两步操作:先将 value 赋值给 p,然后检查 p 的类型。注意,每个分支的变量 p 都是独立的,它们之间没有关系。p : Student 代表 " 如果 p 是 Student 类型 "。之前我们曾使用 isInstanceOf[] 和 asInstanceOf[]实现过类似的功能,而现在只需通过类型匹配就可以解决问题。
Scala 会拒绝明显不合理的模式匹配并提示语法错误。比如下面的模式匹配猜测 person 可能是一个字符串类型,但显然 person 只可能是 Student 或者是它的某一种派生子类。
val person: Person = new Student
val value = person match {
case p: Student => println("他是一名学生。")
1
case p: String => println("这一个无效的匹配。")
2
}
另外,如果只对person进行类型检查,而不想使用被赋予的值,则可以使用 _ 将这个变量隐藏掉。
val person: Person = new Student
val value = person match {
//这种写法表示只判断value的类型。
case _: Student => println("他是一名学生。")
1
case _: Teacher => println("他是一名老师。")
2
case _: Doctor => println("他是一名医生。")
3
case _ => println("他是一名神秘客,应该是Anonymity类型。")
4
}
print(s"模式匹配返回结果:$value")
2.1 类型擦除影响判断的情形
Java 的泛型擦除机制同样影响了 Scala。这个规则还适用于所有使用了泛型的 Scala 集合:Set,List ,Map,ArrayBuffer ... 但不包含 Array (下文介绍原因)。观察下面的代码,无论传入何种泛型的 Map[K,V] ,由于无法判断 Map 的具体类型,该模式匹配总是在第一条 case 就返回了。
val stringToInt = Map("1" -> 2)
val intToString = Map(2 -> "1")
def checkMap(any: Any): Unit = {
any match {
//由于类型擦除的缘故,这个模式匹配从 "判断何种类型的 Map" 退化为 "判断是不是 Map 类"
case _: Map[String, String] => println("Map[String,String]")
case _: Map[Int, Double] => println("Map[Int,Double]")
case _: Map[Double, Int] => println("Map[Double,Int]")
case _ => println("Map[?,?]")
}
}
checkMap(stringToInt)
checkMap(intToString)
这个问题需要依赖更强大的类型系统来解决。我们在后续的反射章节——运行时反射部分介绍如何使用 TypeTag 将 "类" 和 "型" 一同提取出来。
但是 Array 类型是个例外,模式匹配能直接分辨出 Array[T] 类型 ,原因其实是 Scala 将所有的 Array[T] 翻译成了 Java 中对应的 T[] 类型。
val ints: Array[Int] = Array[Int](1,2,3)
val strings: Array[String] = Array[String]("1","2","3")
def checkMap(any: Any): Unit = {
any match {
case _: Array[Int] => println("Array[Int]")
case _: Array[String] => println("Array[String]")
}
}
checkMap(ints)
checkMap(strings)
//scala的Array[Int] => Java 的 int[]
println(ints.getClass)
//scala的Array[String] =>Java 的 String[]
println(strings.getClass)
3. 对象匹配
除了对类型的匹配,match 语句还可以精确到对象的匹配,并提取出内部的数据。我们先讨论匹配数组,列表,元组的情形,然后再讨论对其它对象的匹配。
3.1 匹配数组
利用 match 语句对数组类型(比如 Array,ArrayBuffer)进行匹配更加有效率,且对代码可读性的提升是巨大的。这里实际上已经涉及到了提取器相关的内容,不过这并不影响我们从下述的代码块中 "意会" 这个模式匹配想表达的意思:比如 case Array(1,2,3) 表示判断 ints 是不是一个 Array(1,2,3) 样式的数组。当然,这还隐含了一个前提条件:ints 首先要是一个 Array 类型的数组。
除此之外可以组合 _ ,_* 等符号来规定匹配方式。_ 符号我们已经非常熟悉了,而 _* 则表示**"之后的任意个元素"**。注意,_* 符号只能放在最后面。
val ints: Array[Int] = Array[Int](1,2)
ints match {
case Array(1,2,3) =>println("这个数组有1,2,3。")
case Array(_,_) => println(s"这个数组包含两个元素。")
case Array(0,_*) => println(s"这个数组以0开头。")
case Array(1,2,3,_*) => println(s"这个数组以1,2,3开头。")
case _ => println("都不匹配规则。")
}
试想一下,如果要使用 Java 来表达 "数组有两个元素",”开头是1,2,3“ 甚至诸如 "中间的元素是3" 这样的判断条件写起来会有多么麻烦。除此之外,利用模式匹配我们可以轻松地实现元素交换:
ints match {
case Array(0) => Array(1,2,3)
case Array(x,y) => Array(y,x)
case Array(x,y,z) => Array(x,z,y)
case _ if ints.length>5 => Array(ints(0),ints(1),ints(2))
case _ =>Array(0)
}
题外话,对于 _* 符号还有另一种用法。设有一个可变参数的函数:
def ints(ints: Int*) : Unit = println(ints)
在某些情况下,我们会希望将某个 Seq 序列内的所有元素作为不定参数传入 ints 当中。然而下面的写法并不能通过:
val seq = Seq(1,2,3)
ints(seq)
编译器认为,参数列表希望得到多个 Int 类型,而我们却直接传入了 Seq[Int] 整个序列。为了消除这样的误会,我们使用 _* 符号来表示将序列内的元素依次取出,并放入到参数列表当中。
ints(seq:_*)
3.2 匹配列表
对于列表匹配,还可以使用:: 或者 ::: ( 这两个符号曾经在集合的 List 章节介绍过,它们实际上是提取器,但是这不妨碍我们以直观的形式使用它们 ) 符号组合出高度抽象的列表匹配模式。
val ints = List[Int](1,2,3,4,5,6)
ints match {
case List(1, 2) => println("这个数组就是 (1,2)")
case List(1, _*) => println("这个数组以1开头")
//匹配list,还可以使用::连接符表示要匹配的数组
case 1 :: _ :: Nil => println("这个数组以1开头,后面还有1个元素。")
case 1 :: _ => println("这个数组以1开头,但并不关心它后面有多少个元素。")
case _ :: 2 :: Nil => println("这个数组有两个元素,其中第二个元素为2。")
case _ :: tail => println("会得到剩下的2,3,4,5,6")
case 2 :: left =>
println("这个数组以2开头,剩下的元素是:")
for (i <- left) println(i)
case _ => println("不满足以上任何一个条件。")
}
3.3 匹配元组
下面给出匹配元组的代码示例。注意,元组不等同于列表或者数组,这里不能使用 _* 符号表示 "不确定的元素数量 ",因为每一个元组应当是明确的 TupleX 类型。
tuple2 match {
case (2,"tuple3") => println("该二元组为:(2,\"tuple3\")")
case (2,_) => println("该二元组以2开头。")
case (x,y) => (y,x) //调换二元组的元素。
case _=> println("不满足任何一个匹配的条件")
}
4. 提取器
现在提出一个新的需求,通过模式匹配来判断传入的参数是否是 Student 类型;同时判断它的成绩 grade 是否大于 60;顺便将这个学生的名字也打印出来。下面给定 Student 类的简单定义:
object Student {
def apply(name : String,grade : Int): Student = new Student(name,grade)
}
class Student(var name : String,var grade : Int)
第一种解决方案,可以结合类型匹配和条件守卫表述出这个判断逻辑:
def isStudent(i: Any): Boolean = {
i match {
case i: Student if i.grade >= 60 =>
println(s"this student(${i.name})'s grade is ok."); true
case i: Student if i.grade < 60 =>
println(s"this student(${i.name})'s grade is lower than 60."); true
case _ =>
println("not match"); false
}
}
这样做可行,但是跟前面的匹配数组,匹配列表等过程相比,这种写法有些啰嗦。为什么不可以像之前那样,直接使用 case Student(grade,name) 这样的写法来提取出这个对象内部的属性呢?或者这样问,为什么数组,列表,元组可以直接用 Array(x,y,z) ,List(x,y,z) 或是 (x,y,z)的形式直接提取元素呢?
这些类本身实现了 提取器 的功能,因此支持在模式匹配时直接提取内部的元素。对于自定义的类,我们也需要将它们设计成提取器才行。
4.1 unapply 方法
提取器指代那些具备 unapply 方法的单例对象 ( 或称伴生对象 )。从名字上来看,unapply 方法和之前用于构造对象的 apply 工厂方法是相对的关系。
同样是 Student(name,grade) ,对于 apply 方法而言,这是一步 "注入" 操作:提供 name 和 grade 两个属性,然后该方法返回实例;而对于 unapply 方法而言,这是一个 "提取" 操作:在模式匹配中,如果判断出它属于 Student 类型,则将它对应属性提取到 name 和 grade 两个变量当中。
apply 和 unapply 是对偶关系,或者是互逆的过程。对于提取器而言,可以不定义 apply 方法,但是必须定义 unapply 方法。但是为了满足这种对偶性,提取器一般也会实现 apply 工厂方法。如下面的调用:
Student.unapply(Student.apply(name,grade))
应当返回一个:Some((name,grade)) ,Some 是从属于 Option 的包装类 。如果提取的属性有 2 个或以上,则这些元素还会被包装到元组当中。但如果提取的属性只有1个,那么这单个元素会被直接包装到 Some 当中。在模式不匹配的情况下, unapply 方法应当返回 None 。
以前文匹配 ints 数组的元素交换为例,我们可以想象模式匹配实际上是在这样做:
/*
ints match{
case Array(x,y) => Array(y,x)
...
}
*/
Array.unapply(int) match{
case Some((x,y)) => Array.apply(y,x)
...
}
如果模式匹配成功将 int 按照 Array 的 unapply 方法拆解,那么它就可以正确地返回对应的属性。
回归到案例中,现在我们可以针对 Student 类设计出这样 unapply 方法来供模式匹配时调用。注意,Option 内包装了元组,返回结果之前应该弄清每个位置的元素都代表哪些属性。
def unapply(arg: Student): Option[(String, Int)] = Some(arg.name,arg.grade)
这样一来,我们就能重新以简明的形式直接对 Student 类的对象进行属性提取了:
def isStudent(i: Any): Boolean = {
i match {
// 提取值将赋值给 name 和 grade 。
case Student(name,grade) if grade >= 60 =>
println(s"this student($name)'s grade is ok."); true
case Student(name,grade) if grade < 60 =>
println(s"this student($name)'s grade is lower than 60."); true
case _ =>
println("not match"); false
}
}
unapply 也有不提取任何元素的情况,此时它的返回值从 Option 退化到 Boolean 类型。该提取器不会为模式匹配提供任何提取值,而仅仅起到判断作用。比如我们直接将判断成绩的逻辑封装在 unapply 方法内:
def unapply(arg: Student): Boolean = if(arg.grade >= 60) true else false
外部的模式匹配也不再需要任何提取值和条件守卫了。同时,由于 unapply 方法不提供提取值,因此后面跟上了一个空括号。这个空括号是必须的,否则就成变量 i 和单例对象 Student 的匹配了。
def isStudent(i: Any): Boolean = {
i match {
case Student() => println("this student's grade is ok.");true
case _ =>
println("not match"); false
}
}
但对于这个案例而言,我们不应该将判断逻辑隐藏在Student的 unapply 方法内部。对于不知道 unapply 内部细节的代码调用者来讲,他并不知道 case Student() => 到底想表达什么意思。我们最好另创建一个提取器,规范它的命名以暗示此提取器的功能:
object Student {
object passedExam{
def unapply(arg: Student): Boolean = arg.grade>=60
}
def apply(name: String, grade: Int): Student = new Student(name, grade)
def unapply(arg: Student): Boolean = if(arg.grade >= 60) true else false
}
这样一来,模式匹配的可读性就比刚才强很多了。
i match {
case Student.passedExam() => println(s"this student's grade is ok.");true
case _ => println("not match"); false
}
即便如此,笔者也不推荐这样做。 unapply 方法仅仅负责属性的 "提取",不应该在该方法内部做过度的设计。我们应当使用条件守卫来将额外的判断逻辑安排在一个 "更显眼的位置",以此提高代码的可读性。
4.2 unapplySeq 方法
当 unpply 方法提取出的返回值有多个元素,我们应当转而选择用于变长参数匹配的 unapplySeq 方法。比如说下面的提取器能够以 , 符号解析 String 字符串,并提取出对应的单词,而它是使用 unapplySeq 完成的:
object Words{
def unapplySeq(sentence : String) : Option[Seq[String]] = {
if(sentence.contains(",")){
Some(sentence.split(","))
} else None
}
}
由于事先并不知道能够拆分出多少个单词,因此在这里需要以 Option[Seq[String]] 作为返回值。
"hello,world,java and scala" match {
case Words(a,"world",b) => println(s"$a,$b")
}
4.3 @ 符号关联多个提取结果
针对 unapplySeq 方法,如果想将多项提取值关联到一个序列中,可以使用 @ 符号来实现,如:
"hello,python,java and scala" match {
case Words(a, "world", b) => println(s"$a,$b")
// 除了第一个以外的所有单词关联到 tail 变量。 tail 在这里属于 Seq[String].
case Words(_, tail @_*) => tail foreach(println(_))
}
在这个例子中,_* 表示的后续提取值被 @ 符号绑定到了 tail 变量上。
4.4 用反引号表示值匹配
观察下面的简单例子:
def ifEqual(a : Option[Int],v : Int) : Boolean = a match {
case Some(x) if x == v => true
case None => false
}
这段代码的含义是:如果能够从传入的参数 a 当中提取到某个 临时变量 x,那么就再将它和另一个入参 v 进行比较。本质上,我们只希望验证 Some 所包装的值是否为 v 。因此,这段代码有更精简的表述,
def ifEqual(a : Option[Int],v : Int) : Boolean = a match {
case Some(`v`) => true
case None => false
}
带反引号 的 Some(`v`) 表示直接匹配其提取值是否为入参的那个变量 v,这相当于省去了提取到临时变量的过程。
5. 样例类
只有提取器才能够用于对象匹配,如果想要快捷地提取对象属性,我们总是需要手动补齐 unapply 方法。当然,在这之前还需要声明伴生对象 ...... 在绝大部分情况下,这都是重复的工作,就好比我们总是需要为 Java Bean 补齐 get 和 set 方法一样。
class CaseClass(val x : Int , val y : Int)
object CaseClass{
def apply(x : Int,y : Int) : CaseClass = new CaseClass(x,y)
def unapply(arg: CaseClass): Option[(Int, Int)] = Some((arg.x,arg.y))
}
Scala 考虑到了这一点,并为我们提供了一个增强的 样例类 来简化代码,这只需要在类声明的前面添加一个额外的 case 标识符:
case class CaseClass(x : Int,y : Int)
这一行代码和上述的 "大段声明" 是等效的。首先,编译器会自动为我们构造好具有 unapply 和 apply 方法的伴生对象,其次,所有在主构造器中的参数将自动被视为不可变的 val 类型成员。这样简短的声明使得样例类声明和对象匹配达到视觉上的高度统一:
case CaseClass(x,y) => ...
除此之外,编译器还会额外地帮我们自动实现 toString ,hashCode 和 equal 方法,以及用于深复制的 copy 方法,下面举一个例子来说明:
val caseClass : CaseClass = CaseClass(1,2)
// 它是指向另外一个对象的引用,但是所有的值和 caseClass 相同。
val caseClass_temp : CaseClass = caseClass.copy()
如果在复制之前要覆盖一些值,也可在 copy 方法的形参列表中指定它。
val caseClass : CaseClass = CaseClass(1,2)
// caseClass(1,10)
val caseClass_temp : CaseClass = caseClass.copy(y = 10)
6. 模式,递归和分治哲学
这里直接使用一个案例来说明。如何利用模式匹配将两个有序序列 list1 和 list2 重新拼接成一个更长的有序列表呢?
// 给定两个有序的数组
val list1 = List(1, 3, 6, 9)
val list2 = List(4, 5, 7, 8)
// 给定待实现的 merge 方法
// 注:??? 是一个在运行时会抛出 scala.NotImplementedError 的方法,我们在编写程序时,可以使用它来做临时占位符号。
def merge(xs: List[Int], ys: List[Int]): List[Int] = ???
// 期望得到的结果是:1,3,4,5,6,7,8,9
println(merge(list1, list2).mkString(","))
在实现这个 merge 方法之前,笔者在这里做一些符号上的声明。对于第一个 List[Int] 类型参数 xs ,假定它的头一个元素为 x1 ,而剩下元素用 x1_~> 来标记。同样,对于参数 ys 也要划分出 y1 和 y1_~> 。
由于条件已经给出:两个列表均是已经有序排列的,因此每次仅需要比较 x1 和 y1 而不用考虑后续部分。如果 x1 < y1 ,则应该将 x1 放到靠前位置,然后从 x1_~> 和 ys 列表中再次重复这个过程,并提取下一个次小的元素( x1 > y1 的情况同理)。
结合模式匹配和 :: 符号,我们很容易就能写出优雅的递归过程。其中还补充了 xs 或者 ys 为 Nil 时的特殊情况:
def merge(xs: List[Int], ys: List[Int]): List[Int] = (xs, ys) match {
case (Nil, _) => ys
case (_, Nil) => xs
case (x1 :: x1_~>, y1 :: y1_~>) =>
if (x1 < y1) x1 :: merge(x1_~>, ys)
else y1 :: merge(y1_~>, xs)
}
实际上,我们只需要在这个 merge 方法的基础之上再稍加完善,一个二路归并排序的 Scala 实现就完成了。归并排序的思路是:将一个长的列表对半分为两个子序列,然后使用 merge 方法进行归并。而为了保证两个子序列是有序的,还需要将分别将两个子序列再次进行分割,归并,如此递归。上述逻辑的 Scala 表述如下:
def mergeSort(xs: List[Int]): List[Int] = {
def merge(xs: List[Int], ys: List[Int]): List[Int] = (xs, ys) match {
case (Nil, _) => ys
case (_, Nil) => xs
case (x1 :: x1_~>, y1 :: y1_~>) =>
if (x1 < y1) x1 :: merge(x1_~>, ys)
else y1 :: merge(y1_~>, xs)
}
val n : Int = xs.length / 2
if(n == 0) xs // xs 已经是不可再分的原子。
else {
val (left, right) = xs splitAt n //splitAt 方法将列表从 (0 ~ n-1) 号索引的元素划分给左半部分,其余划分给右半部分。
merge(mergeSort(left), mergeSort(right))
}
}
在这个例子当中,我们仅使用模式匹配配合 :: 符号就实现了对列表进行了拆分,合并,并通过递归完美地处理了一个分治问题。除此之外,我们可通过 模式匹配 + 递归 的方式来优雅地设计出一个函数式数据结构。
7. 处处皆模式
除了 match 语句之外,Scala 在其它地方也应用了类似模式匹配的风格,比如说偏函数,变量赋值,乃至 for 循环中。
7.1 模式匹配风格的赋值
有时我们会面临这样的初始化变量方式:
val x =1
val y = 2
val z = 3
可以利用 “模式匹配” 的写法来对多个变量进行批量赋值,简化代码,比如:
//等价于:
//val x = 1
//val y = 2
//val z = 3
val (x, y, z) = (1, 2, 3)
模式匹配风格的赋值方式还可以引申到 Array 或者是 List 的情形。
// a = 1
val Array(a, _*) = Array(1, 2, 3, 4)
val list = List(1,2,3,4,5)
// head = 1; b = 2; c= 4; tail = 5;
val head :: b :: 3 :: c :: tail = list
但是要注意,当不能正确地匹配赋值时,同样可能会引发 MatchError 异常。
val list = List(1,2,3,4,5)
val head :: b :: 5 :: c :: tail = list
// Exception in thread "main" scala.MatchError: List(1, 2, 3, 4, 5) (of class scala.collection.immutable.$colon$colon)
7.2 for 循环中的模式
模式匹配还可以用在 for 循环中,比如下面的例子表示从 maps 映射中收集所有 value = "jvm" 的 key 值,并打印它:
val maps = Map("scala" -> "jvm", "java" -> "jvm", "c++" -> "c", "c#" -> "c")
//在for循环中进行模式匹配。
val strings = for ((k, "jvm") <- maps) yield k
println(strings.mkString(","))
8. 偏函数
8.1 形式
我们可以单独提取出模式匹配的 case 语法块,它实际上被称之为偏函数:
// val pf: PartialFunction[Int,String] = {case 1 => "hello,scala"}
// 内部语法遵循模式匹配。
val pf: PartialFunction[Int,Any] = {
case 1 => "hello,scala"
case a : Int => println(a)
// 这里的 case _ 表示通配。
case _ => println("unmatched")
}
不难看出,Scala 对偏函数有明确的类型定义,那就是 PartialFunction,它实际上继承自 Function1 。换句话说,任何能够接收 A => B 类型的高阶函数,同样能够接收偏函数。同时,偏函数也可以像普通函数那样使用 andThen,orElse 等方式自由组合。
在某些场合中,我们可以传递偏函数来实现 "模式匹配" 的效果。典型的应用有集合操作的 collect 和 map 这两个动作。
8.2 partialFunction in collect
当偏函数应用到 collect 方法时,相当于做了 "边筛选边收集" 的动作。值得注意的是:当一个元素没有出现在任何一个 case 分支时,它会被直接丢弃,而非抛出 MatchError。
val f : PartialFunction[Int,Int] = {
case a if a % 2 == 0 => a
}
// 打印 2 和 4.
List(1, 2, 3, 4).collect(f).foreach(println(_))
8.3 partialFunction in map
当偏函数应用到 map 方法时,相比于纯粹的 "单路映射" A => B ,它可以根据多个条件进行 "多路映射"。当一个元素没有出现在任何一个 case 分支时,程序会抛出 MatchError。
val f : PartialFunction[Int,Int] = {
case a if a % 2 == 0 => a * 2
case a if a % 2 == 1 => a * 3
}
// 打印 3, 4, 9, 8
List(1, 2, 3, 4).map(f).foreach(println(_))