Scala 之:模式匹配

2,357 阅读4分钟

模式匹配是 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及以上。")
}

只有在同时满足 caseif 的条件时,程序才会运行对应的代码块。另外,每个函数块可以用缩进形式来代替花括号,在保持代码可读性的同时也让代码变得更加整洁。

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 关键字后面可以带上一个临时的不可变变量,这个临时变量只在当前分支内生效,比如下面代码块的 ab

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 代表 " 如果 pStudent 类型 "。之前我们曾使用 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 集合:SetListMapArrayBuffer ... 但不包含 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 语句对数组类型(比如 ArrayArrayBuffer)进行匹配更加有效率,且对代码可读性的提升是巨大的。这里实际上已经涉及到了提取器相关的内容,不过这并不影响我们从下述的代码块中 "意会" 这个模式匹配想表达的意思:比如 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 方法而言,这是一步 "注入" 操作:提供 namegrade 两个属性,然后该方法返回实例;而对于 unapply 方法而言,这是一个 "提取" 操作:在模式匹配中,如果判断出它属于 Student 类型,则将它对应属性提取到 namegrade 两个变量当中。

applyunapply 是对偶关系,或者是互逆的过程。对于提取器而言,可以不定义 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 按照 Arrayunapply 方法拆解,那么它就可以正确地返回对应的属性。

回归到案例中,现在我们可以针对 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
  }
}

但对于这个案例而言,我们不应该将判断逻辑隐藏在Studentunapply 方法内部。对于不知道 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 补齐 getset 方法一样。

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)

这一行代码和上述的 "大段声明" 是等效的。首先,编译器会自动为我们构造好具有 unapplyapply 方法的伴生对象,其次,所有在主构造器中的参数将自动被视为不可变的 val 类型成员。这样简短的声明使得样例类声明和对象匹配达到视觉上的高度统一:

case CaseClass(x,y) => ...

除此之外,编译器还会额外地帮我们自动实现 toStringhashCodeequal 方法,以及用于深复制的 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. 模式,递归和分治哲学

这里直接使用一个案例来说明。如何利用模式匹配将两个有序序列 list1list2 重新拼接成一个更长的有序列表呢?

// 给定两个有序的数组
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 也要划分出 y1y1_~>

由于条件已经给出:两个列表均是已经有序排列的,因此每次仅需要比较 x1y1 而不用考虑后续部分。如果 x1 < y1 ,则应该将 x1 放到靠前位置,然后从 x1_~>ys 列表中再次重复这个过程,并提取下一个次小的元素( x1 > y1 的情况同理)。

结合模式匹配和 :: 符号,我们很容易就能写出优雅的递归过程。其中还补充了 xs 或者 ysNil 时的特殊情况:

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. 偏函数

一文读懂Scala中的偏函数-51CTO.COM

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 类型的高阶函数,同样能够接收偏函数。同时,偏函数也可以像普通函数那样使用 andThenorElse 等方式自由组合。

在某些场合中,我们可以传递偏函数来实现 "模式匹配" 的效果。典型的应用有集合操作的 collectmap 这两个动作。

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(_))