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)