前言
首先,我们要在一开始强调一件很重要的事:Scala的模式匹配发生在但绝不仅限于发生在match case
语句块中,这是Scala模式匹配之所以重要且有用的一个关键因素!我们会在文章的后半部分详细地讨论这一点。
模式匹配的种类
在Scala中一共有如下几种类型的模式匹配:
- 通配符匹配(Wildcard Pattern Matching )
- 常量匹配 (Constant Pattern Matching )
- 变量匹配(Variable Pattern Matching )
- 构造函数匹配(Constructor Pattern Matching )
- 集合类型匹配(Sequence Pattern Matching )
- 元祖类型匹配(Tuple Pattern Matching )
- 类型匹配(Typed Pattern Matching )
一个包罗万象的例子
让我们来看一下几乎展示了所有类型的模式匹配的例子:
object PatternMatchingDemo {
case class Person(firstName: String, lastName: String)
case class Dog(name: String)
def echoWhatYouGaveMe(x: Any): String = x match {
// constant patterns
case 0 => "zero"
case true => "true"
case "hello" => "you said ‘hello‘"
case Nil => "an empty List"
// sequence patterns
case List(0, _, _) => "a three-element list with 0 as the first element"
case List(1, _*) => "a list beginning with 1, having any number of elements"
case Vector(1, _*) => "a vector starting with 1, having any number of elements"
// tuples
case (a, b) => s"got $a and $b"
case (a, b, c) => s"got $a, $b, and $c"
// constructor patterns
case Person(first, "Alexander") => s"found an Alexander, first name = $first"
case Dog("Suka") => "found a dog named Suka"
// typed patterns
case s: String => s"you gave me this string: $s"
case i: Int => s"thanks for the int: $i"
case f: Float => s"thanks for the float: $f"
case a: Array[Int] => s"an array of int: ${a.mkString(",")}"
case as: Array[String] => s"an array of strings: ${as.mkString(",")}"
case d: Dog => s"dog: ${d.name}"
case list: List[_] => s"thanks for the List: $list"
case m: Map[_, _] => m.toString
// the default wildcard pattern
case _ => "Unknown"
}
def main(args: Array[String]) {
// trigger the constant patterns
println(echoWhatYouGaveMe(0))
println(echoWhatYouGaveMe(true))
println(echoWhatYouGaveMe("hello"))
println(echoWhatYouGaveMe(Nil))
// trigger the sequence patterns
println(echoWhatYouGaveMe(List(0,1,2)))
println(echoWhatYouGaveMe(List(1,2)))
println(echoWhatYouGaveMe(List(1,2,3)))
println(echoWhatYouGaveMe(Vector(1,2,3)))
// trigger the tuple patterns
println(echoWhatYouGaveMe((1,2))) // two element tuple
println(echoWhatYouGaveMe((1,2,3))) // three element tuple
// trigger the constructor patterns
println(echoWhatYouGaveMe(Person("Melissa", "Alexander")))
println(echoWhatYouGaveMe(Dog("Suka")))
// trigger the typed patterns
println(echoWhatYouGaveMe("Hello, world"))
println(echoWhatYouGaveMe(42))
println(echoWhatYouGaveMe(42F))
println(echoWhatYouGaveMe(Array(1,2,3)))
println(echoWhatYouGaveMe(Array("coffee", "apple pie")))
println(echoWhatYouGaveMe(Dog("Fido")))
println(echoWhatYouGaveMe(List("apple", "banana")))
println(echoWhatYouGaveMe(Map(1->"Al", 2->"Alexander")))
// trigger the wildcard pattern
println(echoWhatYouGaveMe("33d"))
}
}
相应的输入如下:
zero
true
you said ‘hello‘
an empty List
a three-element list with 0 as the first element
a list beginning with 1, having any number of elements
a list beginning with 1, having any number of elements
a vector starting with 1, having any number of elements
got 1 and 2
got 1, 2, and 3
found an Alexander, first name = Melissa
found a dog named Suka
you gave me this string: Hello, world
thanks for the int: 42
thanks for the float: 42.0
an array of int: 1,2,3
an array of strings: coffee,apple pie
dog: Fido
thanks for the List: List(apple, banana)
Map(1 -> Al, 2 -> Alexander)
you gave me this string: 33d
上述示例中唯一没有展示的是变量模式匹配。变量模式匹配很像通配符模式匹配,唯一的区别在于:使用通配符模式匹配时,你不能在case推导符后面使用匹配到的值,但是变量模式匹配给匹配到的值命名了一个变量名,因此你可以在推导符后面使用它。下面这个例子就演示了变量模式匹配:
scala> def variableMatch(x:Any):String =x match {
| case i:Int => s"This is an Integer: $i"
| case otherValue => s"This is other value: $otherValue" //You can use var: otherValue.
| }
variableMatch: (x: Any)String
scala> println(variableMatch(1))
This is an Integer: 1
scala> println(variableMatch(1.0))
This is other value: 1.0
scala> println(variableMatch("SSS"))
This is other value: SSS
模式匹配的附加约束(Guard)
上述7种模式匹配是语法层面上的模式匹配,很多时候,只有这7种模式匹配是不够的,程序员需要根据具体的值做更细致的匹配,这时,我们需要对模式匹配附加更多的约束条件,这些约束条件叫做Guard,对应到代码上就是在case后面再添加if语句用于对匹配做更加细致的描述。让我们来看一个例子:
scala> def testPatternGuard(x: (Int,Int)):Int = x match {
| case (a,a)=>a*2
| case (a,b)=>a+b
| }
<console>:8: error: a is already defined as value a
case (a,a)=>a*2
^
上述代码的设计初衷是希望通过模式匹配来判断二元元组中的两个值是不是一样,如果是一样的,使用一种计算逻辑,如果不一样则使用另一个计算逻辑,但是这段代码是不能编译通过的,Scala要求“模式必须是线性的”,也就是说:模式中的变量只能出现一次。(Scala restricts patterns to be linear: a pattern variable may only appear once in a pattern.)在这个例子中寄希望使用一个变量让Scala在编译时帮助你判断两个值是否一值显然是做不到的,所以必然会报错,在这种场合就是需要使用if语句来限定匹配条件的时候了,以下正确的做法:
scala> def testPatternGuard(x: (Int,Int)):String = x match {
| case (a,b) if a==b =>s"a==b,so, we can calc it as: a*2=${a*2}"
| case (a,b)=>s"a!=b,just calc it as: a+b=${a+b}"
| }
testPatternGuard: (x: (Int, Int))String
scala> println(testPatternGuard((1,2)))
a!=b,just calc it as: a+b=3
scala> println(testPatternGuard((1,2)))
a!=b,just calc it as: a+b=3
Sealed Classe与模式匹配
如果一个类被声明为sealed,则除了在定义这个class的文件内你可以创建它的子类之外,其他任何地方都不允许一个类去继承这个类。在进行模式匹配时,我们需要时刻留心你的case语句是否能cover所有可能的情形,但如果在匹配一个类族特别是子类时,可能会出现无法控制的情况,因为如果类族是可以自由向下派生的话,过去覆盖了各种情形的case语句就可能不再“全面”了。所以使用sealed class是对模式匹配一种保护。另外,使用sealed class还可以从编译器那边得到一些额外的好处:当你试图针对case继承自sealed class的case类进行模式匹配时,如果漏掉了某个某些case类,编译器在编译时会给一个warning. 所以说:当你想为一模式匹配而创建一个类族时,或者说你的类族将要被广发使用于模式匹配时,你最好考虑将你的类族超类限定为sealed。比如,当你定义这样一组sealed classes时:
sealed abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String,left: Expr, right: Expr) extends Expr
如果你写了这样一个模式匹配:
scala> def describe(e: Expr): String = e match {
| case Number(_) => "a number"
| case Var(_) => "a variable"
| }
<console>:12: warning: match may not be exhaustive.
It would fail on the following inputs: BinOp(_, _, _), UnOp(_, _)
def describe(e: Expr): String = e match {
^
describe: (e: Expr)String
编译器就会给你一个warnining。
模式匹配无处不在
上面我们演示的所有模式匹配都是基于match case
语句块的,诚如我们在文章一开始就强调的:如果模式匹配仅仅存在于match case
语句中,那这项优秀特性的辐射的能量将会大打折扣,Scala正是将模式匹配发扬到编程的方方面面,才使得模式匹配在Scala里真正地大放异彩。
变量定义中的模式匹配
这可能是Scala的模式匹配最吸引人的地方了,在Scala里,每当你定义一个变量时,你可以直接利用模式匹配同时为多个变量一次性赋值!这一特性被广泛使用于从元组,Case类和构造器中提取对应的值赋给多个变量。以下展示了几种常见的示例:
从元组中提取变量
scala> val (number,string)=(1,"a")
number: Int = 1
string: String = a
scala> println(s"number=$number")
number=1
scala> println(s"string=$string")
string=a
从构造器中提取额变量
scala> case class Person(name:String,age:Int)
defined class Person
scala> val Person(name,age)=Person("John",30)
name: String = John
age: Int = 30
scala> println(s"name=$name")
name=John
scala> println(s"age=$age")
age=30
一个更常见的例子是在main函数中提取命令行传递过来的参数列表:
def main(args: Array[String]) {
val Array(arg1,agr2)=args
.....
}
case语句块(函数字面量)中的模式匹配
在Scala之偏函数Partial Function 一文中我们详细介绍了偏函数,其中提到使用不含match的case语句块可以构建一个偏函数的字面量,这个偏函数具有多个“入口”,每一个入口都由一个case描述,这样在调用一个偏函数时,根据传入的参数会匹配到一个case,这个过程也是模式匹配,这种模式匹配和match case 的模式匹配是很相似的。
for循环中的模式匹配
如果我们认为for循环中声明的局部迭代变量就是一个普通变量,那么在for循环中使用的模式匹配实质上就是前面提到的变量定义中使用的模式匹配,来看一个列子:
scala> val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
capitals: scala.collection.immutable.Map[String,String] = Map(France -> Paris, Japan -> Tokyo)
scala> for ((country, city) <- capitals)
| println("The capital of "+ country +" is "+ city)
The capital of France is Paris
The capital of Japan is Tokyo
更深的理解
为什么我们需要“模式匹配”?
在一次对Martin Odersky的采访中,Martin Odersky这样解释到:
我们每个人都有复杂的数据。如果我们坚持严格的面向对象的风格,那么我们并不希望直接访问数据内部的树状结构。相反,我们希望调用 方法,然后在方法中访问。如果我们能够这样做,那么我们就再也不需要模式匹配了,因为这些方法已经提供了我们需要的功能。但很多情况下,对象并不提供我们需要的方法,而且我们无法(或者不愿)向这些对象添加方法。….. 从本质上讲,当你从外部取得具有结构的对象图时,模式匹配就必不可少。你会在若干情况下遇到这种现象。
在这面这段论述中,以及Martin Odersky举例讲到的从XML构建一个DOM类型结构,都无不让我联想到我之前写过的文章:一段关于”多态”的沉思,在这篇文章里我所思索的问题,其实正是应用模式匹配的绝佳场景!
将模式匹配应用于提取一堆值,这么好用,为什么不用呢?
就像某种反向的表达式。正向的表达式向结果中插入值,反向的表达式却是给定结果,一旦匹配成功,就能反过来从结果中抽取出一大堆值。