《快学Scala》第14章——模式匹配和样例类 学习笔记

时间:2020-11-25 05:48:53

Scala有一个十分强大的模式匹配机制,可以应用在很多场合:switch语句、类型查询,以及“析构”(获取复杂表达式中的不同部分)。除此之外,Scala还提供了样例类,对模式匹配进行了优化。
本章要点概述:

  • match表达式是一个更好的switch,不会有意外掉入到下一个分支的问题。
  • 如果没有模式能够匹配,会抛出MatchError。可以用case _模式避免。
  • 模式可以包含一个随意定义的条件,成为守卫。
  • 可以对表达式的类型进行匹配;优先选择模式匹配而不是isInstanceOf/asInstanceOf。
  • 可以匹配数组、元组和样例类的模式,然后将匹配到的不同部分绑定到变量。
  • 在for表达式中,不能匹配的情况会被安静地跳过。
  • 样例类是编译器会为之自动产出模式匹配所需要的方法的类。
  • 样例类继承层级中的公共超类应该是sealed的。
  • 用Option来存放对于可能存在也可能不存在的值——这比null更安全。

14.1 更好的switch

以下是Scala中C风格的switch语句的等效代码:

var sign = ...
val ch: Char = ...
ch match{
case '+' => sign = 1
case '-' => sign = -1
case _ => sign = 0
}

与default等效的是捕获所有的case 模式。如果没有模式能匹配,代码会抛出MatchError,所以在考虑到各种可能的异常后带上捕获所有的case 模式,是一个不错的选择。
与switch语句不同,Scala模式匹配并不会有“意外掉入到下一个分支”的问题。(在C和其他类C语言中,必须在每个分支的末尾显示地使用break语句退出switch,否则将掉入到下一个分支,这一点是很容易出错的。)
与if类似,match也是表达式,而不是语句。上面的代码可以简化为:

sign = ch match{
case '+' => 1
case '-' => -1
case _ => 0
}

可以在match表达式中使用任何类型,而不仅仅是数字。例如:

color match{
case Color.RED => ...
case Color.BLACK => ...
...
}

14.2 守卫

想要扩展示例以匹配所有的情况,在Scala中,需要给模式添加守卫,如下:

ch match{
case '+' => sign = 1
case '-' => sign = -1
case _ if Character.isDigit(ch) => digit = Character.digit(ch, 10)
case _ => sigh = 0
}

守卫可以是任何Boolean条件。注意模式总是从上往下进行匹配。如果带守卫的这个模式不能匹配则捕获所有的模式(case _)会被用来尝试进行匹配 。

14.3 模式中的变量

如果case关键字后面跟着一个变量名,那么匹配的表达式会被赋值给那个变量。例如:

str(i) match{
case '+' => sign = 1
case '-' => sign = -1
case ch => digit = Character.digit(ch, 10)
case _ => sigh = 0
}

可以将case 看做是一个特殊的情况:只不过变量名是罢了。也可以在守卫中使用变量:

str(i) match{
case ch if Character.isDigit(ch) => digit = Character.digit(ch, 10)
...
}

注意:变量模式可能会与常量表达式向冲突,例如:

import scala.math._
x match {
case Pi => ...
...
}

Scala是如何知道Pi是常量而不是变量的?Scala中的规则:变量必须以小写字母开头。如果有一个小写字母开头的常量,则需要将它包在反引号中

import java.io.File._
str match{
case `pathSeparator` => ...
...
}

14.4 类型模式

可以对表达式的类型进行匹配:

obj match {
case x: Int => x
case s: String => Integer.parseInt(s)
case _: BigInt => Int.MaxValue
case _ => 0
}

注意模式中的变量名:在第一个模式中,匹配到的值被当做Int绑到x;而第二个模式中,值被当做String绑定到s。这里不需要用asInstanceOf做类型转换。
在Scala中,更倾向于使用上面的模式匹配,而不是isInstanceOf操作符。
注意:当在匹配类型的时候,必须给出一个变量名。否则,将会拿对象本身进行匹配:

obj match{
case _: BigInt = > Int.MaxValue //匹配任何类型为BigInt的对象
case BigInt => -1 //匹配类型为Class的BigInt对象

注意:匹配发生在运行期,Java虚拟机中反省的类型信息是被擦掉的。因此,不能用类型来匹配特定的Map类型。
case m: Map[String, Int] => … //不要这样做。
可以匹配一个通用的映射:
case m: Map[, ] => … //ok

14.5 匹配数组、列表和元组

匹配数组的内容时,可以在模式中使用Array表达式:

arr match {
case Array(0) => "0"
case Array(x, y) => x + " " + y
case Array(0, _*) => "0 ..."
case _ => "something else"
}

shell运行过程

scala> var arr = new Array[Int](10)
arr: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
scala> arr.
apply asInstanceOf clone isInstanceOf length toString
update

scala> arr.update(1, 12)

scala> arr
res8: Array[Int] = Array(0, 12, 0, 0, 0, 0, 0, 0, 0, 0)

scala> arr match {
| case Array(0) => "0"
| case Array(x, y) => x + " " + y
| case Array(0, _*) => "0 ..."
| case _ => "something else"
| }
res9: java.lang.String = 0 ...

第一个模式匹配包含0的数组,第二个模式匹配任何带有两个元素的数组,并并这两个元素分别绑定到变量x和y;第三个表达式匹配任何以0开始的数组。
可以使用同样的方式匹配列表,使用List表达式,或者使用::操作符:

lst match {
case 0 :: Nil => "0"
case x::y::Nil => x + " " + y
case 0::tail => "0 ..."
case _ => "something esle"
}

对于元组,可以在模式中使用元组表示法:

pair match {
case (0, _) => "0 ..."
case (y, 0) => y + " 0"
case _ => "neither is 0"
}

注意变量是如何绑定到列表或元组的不同部分的。由于这种绑定可以很轻松地访问复杂结构的各组成部分,因此这样的操作被称为“析构”。

14.6 提取器

在上一节模式匹配数组、列表和元组等功能的背后是提取器(extractor)机制——带有从对象中提取值的unapply或unapplySeq方法的对象。unapply方法用于提取固定数量的对象;而unapplySeq提取的是一个序列,可长可短。
正则表达式是另一个适合使用提取器的场景。如果正则表达式有分组,可以用提取器匹配每个分组。

val pattern = "([0-9]+) ([a-z]+)".r
var str = "99 bottles"
str match {
case pattern(num, item) => "匹配的数字字符串是:" + num + item
//将num设为“99”,item设为“bottles”
case _ => "没有数字或字符串相匹配"
}

在执行上面的代码时,pattern变量调用了unapplySeq方法,以被执行匹配动作的表达式作为参数,pattern.unapplySeq(“99 bottles”)产出一些列匹配分组的字符串。这些字符串被分别赋值给变量num和item。在这里提取提并不是一个伴生对象,而是一个正则表达式。

14.7 变量声明中的模式

模式匹配在变量声明中也很有用。/%方法返回包含商和余数的对偶,而这两个值分别被变量q和r捕获到。

scala> val (x, y) = (1, 2)
x: Int = 1
y: Int = 2

scala> val (q, r) = BigInt(10) /% 3
q: scala.math.BigInt = 3
r: scala.math.BigInt = 1

scala> arr
res8: Array[Int] = Array(0, 12, 0, 0, 0, 0, 0, 0, 0, 0)
scala> val Array(first, second, _*) = arr
first: Int = 0
second: Int = 12

14.8 for表达式中的模式

可以在for推导式中使用带变量的模式。对每一个遍历到的值,这些变量都会被绑定。这样可以方便地遍历映射:

import scala.collection.JavaConversions.propertiesAsScalaMap
//将Java的Properties转换为Scala映射
for((k, v) <- System.getProperties())
println(k + " -> " + v

对映射中的每一个(键,值)对偶,k被绑定到键,而v被绑定到值。在for推导式中,失败的匹配将被安静地忽略。

scala> var buffer = new scala.collection.mutable.ArrayBuffer[Int]()
buffer: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer()

scala> var map = scala.collection.mutable.Map[Int, Int]()
map: scala.collection.mutable.Map[Int,Int] = Map()

scala> map += (1 -> 1, 1 -> 2, 1-> 3, 2 -> 1, 2 -> 2, 2 -> 3, 3 -> 1, 3 -> 2, 3 -> 3)
res11: scala.collection.mutable.Map[Int,Int] = Map(3 -> 3, 1 -> 3, 2 -> 3)

scala> for((k, v) <- map){
| buffer += (k, v)
| println(buffer)
| }
ArrayBuffer(3, 3)
ArrayBuffer(3, 3, 1, 3)
ArrayBuffer(3, 3, 1, 3, 2, 3)

在for推导式中,也可是使用守卫。在下面的例子中,注意if关键字出现在<-之后:

for((k, v) <- System.getProperties() if v == "")
println(k)

14.9 样例类

样例类是一种特殊的类,它们经过优化以被用于模式匹配。如下,表示有两个扩展自常规(非样例)类的样例类:

abstract class Amount
case class Dollar(value: Double) extends Amount
case class Current(value: Double, unit: String) extends Amount

//也可以有针对单例的样例对象:
case Object Nothing extends Amount
//当有一个类型为Amount的对象时,可以用模式匹配到它的类型,并将属性值绑定到变量:
amt match {
case Dollar(v) => "$" + v
case Currency(_, u) => "Oh noes, I got " + u
case Nothing => ""
}

注意:样例类的实例使用(),样例对象不适用圆括号。当声明样例类时,有以下几件事自动发生:

  • 构造器中的每一个参数都成为val——除非它被显式地声明为var(但是不建议这么做)。
  • 在伴生对象中提供apply方法,不用new关键字就可以构造出相应的对象,比如Doullar(29.66)或Currency(29.63, “EUR”)。
  • 提供unapply方法让模式匹配可以工作。
  • 将生成toString、equals、hashCode和copy方法——除非显式地给出这些方法的定义。
    除上述外,样例类和其他类完全一样,可以添加方法和字段,扩展它们。

14.10 copy方法和带名参数

样例类的copy方法创建一个与现有对象值相同的新对象。这个方法本身并不是很有用——毕竟,Currency对象时不可变的,完全可以共享这个对象引用。不过,可以使用带名参数修改某些属性:

val amt = Current(29.92, "EUR")
val price = amt.copy()
val price = amt.copy(value = 19.95) //使用带名参数修改value属性, Currency(19.95, "EUR")
val price = Currency(unit = "CHF") //Currency(29.95, "CHF")

14.11 case语句中的中置表示法

如果unapply方法产生一个对偶,可以在case语句中使用中置表达式尤其,对于有两个参数的样例类,可以使用中置表达式法来表示它。
amt match { case a Currency u => … } //等同于case Currency(a, u)
每个List对象要么是Nil,要么是样例类::,定义如下:

case class :: [E](head: E, tail: List[E]) extends List[E]
//可以写为
lst match { case h :: t => ... }
//等同于case ::(h, t), 将调用::unapply(lst)

在将解析结果组合在一起的~样例类中,本意同样是以中置表达式的形式用于case语句:

result match { case p ~ q => ...} //等同于case ~(p, q)

//如果操作符以冒号结尾,则它是从右向左结合的
case first :: second :: rest //等同于case ::(first, ::(second, rest))

14.12 匹配嵌套结构

样例类经常被用于嵌套结构。例如,某个商店售卖的商品,有时会将物品捆绑在一起打折出售:

abstract class Item
case class Article(description: String, price:Double) extends Item
case class Bundle(description: String, discout: Double, item: Item*) extends Item
//因为不用使用new,所有可以很容易地给出嵌套对象定义:
Bundle("Father's day special", 20.0, Article("Scala for the Impatient", 39.93), Bundle("Anchor Distillery Sample", 10.0, Article("Old Potrero Straight Rye Whisky", 79.92), Article("Juniper Gin", 32.95)))
//模式可以匹配到特定的嵌套:
case Bundle(_, _, Article(descr, _), _*) => ...
//上述代码将descr绑定到Bundle的第一个Article的描述

//也可以使用@表示法将嵌套的值绑定到变量:
case Bundle(_, _, art @ Article(_, _), rest @ _*) => ...
//art就是Bundle中的第一个Article,而rest是剩余Item的序列。注意,本例中的_*是必需的。

14.13 样例类是邪恶的吗

样例类适用于那种标记不会改变的结构,当用在合适的地方时,样例类是十分便捷的,原因如下:

  • 模式匹配通常比继承更容易引向更精简的代码。
  • 构造时不需要用new的复合对象更加易读。
  • 将同时得到toString、equals、hashCode和copy方法,其作用更通常用的功能一样——打印、比较、哈希、拷贝所有字段。
    注意:对于那些扩展其他样例类的样例类而言,toString、equals、hashCode和copy方法不会被生成。如果有一个这样的样例继承自其他样例类,将得到一个编译器警告。

14.14 密封类

当使用样例类做模式匹配时,可能是想让比一期帮你确保你已经列出了所有可能的选择。要达到这个目的,需要将样例类的通用超类声明为sealed:

sealed abstract class Amount
case class Dollar(value: Double) extends Amount
cae class Currency(value: Double, unit: String) extends Amount

//如果有人想要为欧元添加另一个样例类,则他们必须在Amount被声明的那个文件中完成:
case class Euro(value:Double) extends Amount

密封类的所有子类都必须在与该密封类相同的文件中定义。如果某个类是密封的,那么在编译期所有子类就是可知的,因而编译器可以检查模式语句的完整性。让所有(同一组)样例类都扩展某个密封的类或特质是个好的做法。

14.15 模拟枚举

样例类让你可以在Scala中模拟出枚举类型:

sealed abstract class TrafficLightColor
case object Red extends TrafficLightColor
case object Yellow extends TrafficLightColor
case object Green extends TrafficLightColor

color match {
case Red => "stop"
case Yellow => "hurry up"
case Green => "go"
}

14.16 Option类型

标准类库中的Option类型用样例类来表示那种可能存在、也可能不存在的值。样例子类Some包装了某个值,例如: Some(“Fred”),而样例对象None表示没有值。
这比使用空字符的意图更加清晰,比使用null表示缺少某值的做法更加安全。
Option支持泛型,举例,Some(“Fred”)的类型为Option[String]。
Map类的get方法返回一个Option,如果对于给定的键没有对应的值,则get返回None。如果有值,就会将该值包在Some中返回。

scores.get("Alice") match{
case Some(score) => println(score)
case None => print("No score")
}

//也可以使用isEmpty和get:
val alicesScore = scores.get("Alice")
if(alicesScore.isEmpty) println("No score")
else println(alicesScore.get)

//使用getOrElse方法更简洁:
println(alicesScore.getOrElse("No score"))

shell 运行过程:

import scala.collection.mutable
scala> val m = mutable.Map("Alice" -> 87.2, "Tom" -> 78.3, "Jim" -> 65.6)
m: scala.collection.mutable.Map[String,Double] = Map(Jim -> 65.6, Tom -> 78.3, Alice -> 87.2)

scala> val scores = mutable.Map("Alice" -> 87.2, "Tom" -> 78.3, "Jim" -> 65.6)
scores: scala.collection.mutable.Map[String,Double] = Map(Jim -> 65.6, Tom -> 78.3, Alice -> 87.2)

scala> scores.get("Alice") match{
| case Some(score) => print(score)
| case None => println("No score")
| }
87.2

scala> val s = {if(alicesScore.isEmpty) println("No score")
| else println(alicesScore.get)
| }
87.2
s: Unit = ()

scala> alicesScore
res17: Option[Double] = Some(87.2)
scala> println(alicesScore.getOrElse("No score"))
87.2

如果alicesScore为None类型,getOrElse将返回”No score”。Map类也提供了getOrElse方法:

scala> alicesScore
res17: Option[Double] = Some(87.2)

scala> scores
res18: scala.collection.mutable.Map[String,Double] = Map(Jim -> 65.6, Tom -> 78.3, Alice -> 87.2)

scala> println(scores.getOrElse("Alice", "No score"))
87.2

scala> for(score <- scores.get("Alice")) println(score)
87.2

也可以将Option当做一个要么为空、要么带有单个元素的集合,并使用诸如map、foreach或filter等方法。
scores.get(“Alice”).foreach(println _)

14.17 偏函数

被包在花括号内的一组case语句是一个偏函数——一个并非对所有输入值都有定义的函数。它是PartialFunction[A, B]类的一个实例,其中A是参数类型,B是返回类型。该类有两个方法:apply方法从匹配到的模式计算函数值,而isDefinedAt方法在输入至少匹配其中一个模式时返回true。

val f: PartialFuction[Char, Int] = { case '+' => 1; case '-' => -1 }
f('-') //调用f.apply('-'),返回-1
f.isDefinedAt('0') //false
f('0') //抛出MatchError异常

"-3+4".collect { case '+' => 1; case '-' => -1 } //Vector(-1, 1)

有一些方法接受PartialFunction作为参数。如,GenTraversable特质的collect方法将一个偏函数应用到所有在该偏函数有定义的的元素,并返回包含这些结果的序列。

scala> val f: PartialFunction[Char, Int] = {
| case '+' => 1;
| case '-' => -1
| }
f: PartialFunction[Char,Int] = <function1>

scala> f('-')
res22: Int = -1

scala> f.isDefinedAt('0')
res23: Boolean = false

scala> f('0')
scala.MatchError: 0 (of class java.lang.Character)

scala> "-3+4".collect { case '+' => 1; case '-' => -1 }
res25: scala.collection.immutable.IndexedSeq[Int] = Vector(-1, 1)