Scala模式匹配简述
什么是匹配模式?
模式匹配并不很新,(上世纪)七十年代中期就已经有语言采用。据我所知,第一种语言是ML,但可能也有更早的语言支持。它在许多函数式语言中都算是标准功能,包括ML、Caml、Erlang、以及Haskell。
那么什么是模式匹配呢?它可以让你给一个值匹配多种情况,有点像Java中的switch语句。但它不仅可以像switch语句一样用来匹配数字,还可以匹配对象的内在构建形式。
比如,Scala中的List存在两种情况:要么是空List,写做Nil;要么由一个head元素紧接着另一List tail组成。有了模式匹配,你可以询问:给定的List是空List吗?只要编写case Nil、箭头(=>)以及后续表达式即可:
case Nil => // 后续表达式
你还可以询问:它是非空List吗?只要编写case x :: xs、箭头、以及后续表达式即可:
case x :: xs => // 后续表达式
双冒号(::)表示cons操作符;x表示List的首元素,xs表示剩余部分。于是,模式匹配会首先区分List是否为空。而如果List非空,它会把List的首元素命名为x然后把List剩余部分命名为xs。接下来,这些变量可以被箭头右侧表达式所用。(参见示例1)
示例1:match表达式
list match {
case Nil => "was an empty list"
case car :: cdr => "head was " + car + ", tail was " + cdr
}
如果list不为空,将匹配到第二种情况,List首元素将赋值给x,而列表剩余部分赋值给xs。接下来,这些变量将被箭头符号右侧的字符串连接表达式所用。例如,如果list内容是List("hello", "world"),那么匹配表达式的结果将是字符串"head was hello, tail was List(world)"。
上例的模式非常简单。但实际上模式还支持嵌套,类似表达式的嵌套,能让你编写层数很深的模式。总的来说,亮点在于,模式和表达式看起来很像。模式本质上和表达式属于完全一类东西,看上去就像构造表达式一样,可以用来构造复杂树状对象,但却不需要编写new。事实上,在Scala中,该对象构造时一样不需要new。然后你可以在某些位置填上占位变量,对应树对象中实际存在的值。(参见示例2)
示例2:嵌套模式的match表达式
object match {
case Address(Name(first, last), street, city, state, zip) => println(last + ", " + zip)
case _ => println("not an address") // 默认情况
}
在第一种情况下,模式Name(first, last)嵌在模式Address(...)中。last放在了Name构造函数内,可以“提取”出值,因而,可供箭头右边的表达式使用。
因为匹配是发生在运行期的,而且JVM中泛型的类型信息会被擦掉(跟Java里范型一样不能匹配)。
case m: Map[String, Int] => ... // 不行,类型不起作用
case m: Map[_, _] => ... // 匹配通用的Map,OK
但对于数组来说,类型信息是完好的,所以可以在Array上匹配。
对于嵌套结构,举例就能一目了然。
abstarct class Item
case class Article(description: String, price: Double) extends Item
case class Bundle(description: String, price: Double, items: Item*) extends Item
Bundle("Father's day special", 20.0,
Article("Scala for the Impatient", 39.95),
Bundle("Anchor Distillery Sampler", 10.0,
Article("Old Potrero Straight Rye Whisky", 79.95),
Article("Junipero Gin", 32.95)
)
)
模式可以匹配到特定的嵌套:
case Bundle(_, _, Article(descr, _), _*) => ...
上面的代码中descr这个变量被绑定到第一个Article的description。另外还可以使用@来将值绑定到变量:
// art被绑定为第一个Article,rest是剩余的Item序列
case Bundle(_, _, art @ Article(_, _), rest @ _*) => ...
样例类
样例类是种特殊的类,经过优化以用于模式匹配。
abstract class Amount
// 继承了普通类的两个样例类
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount
// 样例对象
case object Nothing extends Amount
使用:
amt match {
case Dollar(v) => "$" + v
case Currency(_, u) => "Oh noes, I got " + u
case Nothing => "" // 样例对象没有()
}
在声明样例类时,下面的过程自动发生了:
构造器的每个参数都成为val,除非显式被声明为var,但是并不推荐这么做;
在伴生对象中提供了apply方法,所以可以不使用new关键字就可构建对象;
提供unapply方法使模式匹配可以工作;
生成toString、equals、hashCode和copy方法,除非显示给出这些方法的定义。
除了上述之外,样例类和其他类型完全一样,方法字段等。
密封类
当使用样例类来做模式匹配时,如果要让编译器确保已经列出所有可能的选择,可以将样例类的通用超类声明为sealed。
密封类的所有子类都必须在与该密封类相同的文件中定义。
如果某个类是密封的,那么在编译期所有的子类是可知的,因而可以检查模式语句的完整性。
让所有同一组的样例类都扩展某个密封的类或特质是个好的做法。
模式匹配的目的
那么,为什么你需要模式匹配?我们每个人都有复杂的数据。如果我们坚持严格的面向对象的风格,那么我们并不希望直接访问数据内部的树状结构。相反,我们希望调用方法,然后在方法中访问。如果我们能够这样做,那么我们就再也不需要模式匹配了,因为这些方法已经提供了我们需要的功能。但很多情况下,对象并不提供我们需要的方法,而且我们无法(或者不愿)向这些对象添加方法。
例如XML。如果给你一棵XML树,那么树就只是单纯的数据。要么是节点,要么是节点的序列。XML是一种非常通用的数据表现形式。例如,DOM本质上只是节点的数组,其中每个节点的类型都未知。现在我们设想一下,如果把XML树转换到某种更强的框架中,可以给你一个列表,容纳各种不同类型的对象。组成列表的元素可能包括诸如电话号码、备忘录或地址等。如果你想以静态类型的方式获取所有这些东西,就会遇上一个问题:你不知道每个元素的类型。在传统面向对象的编程语言中,唯一可行方式是,编写一大堆instanceof检测,一一测试每个元素是PhoneNumber实例、Memo实例,还是其他实例。一旦这些instanceof语句之一检测成功,你还需要进行类型转换。上述做法相当丑陋和笨拙,有了模式匹配就能避免了。模式匹配能以更安全、更自然的方式完成相同功能。
从本质上讲,当你从外部取得具有结构的对象图时,模式匹配就必不可少。你会在若干情况下遇到这种现象,XML是其中之一。各种从文本解析而来的数据,都属于这一类。例如,有一种典型情况下模式匹配必不可少,即,处理编译器中的抽象语法树的情况。如果你要对表达式进行化简操作,表达式会被表示为树,你需要通过模式匹配对这些树进行提取操作。类似那样的情况还有许多。遇到这些情况时,模式匹配真的必不可少。