什么是匹配模式?
模式匹配并不很新,(上世紀(jì))七十年代中期就已經(jīng)有語(yǔ)言采用。據(jù)我所知,第一種語(yǔ)言是ML,但可能也有更早的語(yǔ)言支持。它在許多函數(shù)式語(yǔ)言中都算是標(biāo)準(zhǔn)功能,包括ML、Caml、Erlang、以及Haskell。
那么什么是模式匹配呢?它可以讓你給一個(gè)值匹配多種情況,有點(diǎn)像Java中的switch語(yǔ)句。但它不僅可以像switch語(yǔ)句一樣用來(lái)匹配數(shù)字,還可以匹配對(duì)象的內(nèi)在構(gòu)建形式。
比如,Scala中的List存在兩種情況:要么是空List,寫做Nil;要么由一個(gè)head元素緊接著另一List tail組成。有了模式匹配,你可以詢問(wèn):給定的List是空List嗎?只要編寫case Nil、箭頭(=>)以及后續(xù)表達(dá)式即可:
case Nil => // 后續(xù)表達(dá)式
你還可以詢問(wèn):它是非空List嗎?只要編寫case x :: xs、箭頭、以及后續(xù)表達(dá)式即可:
case x :: xs => // 后續(xù)表達(dá)式
雙冒號(hào)(::)表示cons操作符;x表示List的首元素,xs表示剩余部分。于是,模式匹配會(huì)首先區(qū)分List是否為空。而如果List非空,它會(huì)把List的首元素命名為x然后把List剩余部分命名為xs。接下來(lái),這些變量可以被箭頭右側(cè)表達(dá)式所用。(參見(jiàn)示例1)
示例1:match表達(dá)式
list match {
case Nil => "was an empty list"
case car :: cdr => "head was " + car + ", tail was " + cdr
}
如果list不為空,將匹配到第二種情況,List首元素將賦值給x,而列表剩余部分賦值給xs。接下來(lái),這些變量將被箭頭符號(hào)右側(cè)的字符串連接表達(dá)式所用。例如,如果list內(nèi)容是List("hello", "world"),那么匹配表達(dá)式的結(jié)果將是字符串"head was hello, tail was List(world)"。
上例的模式非常簡(jiǎn)單。但實(shí)際上模式還支持嵌套,類似表達(dá)式的嵌套,能讓你編寫層數(shù)很深的模式??偟膩?lái)說(shuō),亮點(diǎn)在于,模式和表達(dá)式看起來(lái)很像。模式本質(zhì)上和表達(dá)式屬于完全一類東西,看上去就像構(gòu)造表達(dá)式一樣,可以用來(lái)構(gòu)造復(fù)雜樹(shù)狀對(duì)象,但卻不需要編寫new。事實(shí)上,在Scala中,該對(duì)象構(gòu)造時(shí)一樣不需要new。然后你可以在某些位置填上占位變量,對(duì)應(yīng)樹(shù)對(duì)象中實(shí)際存在的值。(參見(jiàn)示例2)
示例2:嵌套模式的match表達(dá)式
object match {
case Address(Name(first, last), street, city, state, zip) => println(last + ", " + zip)
case _ => println("not an address") // 默認(rèn)情況
}
在第一種情況下,模式Name(first, last)嵌在模式Address(...)中。last放在了Name構(gòu)造函數(shù)內(nèi),可以“提取”出值,因而,可供箭頭右邊的表達(dá)式使用。
因?yàn)槠ヅ涫前l(fā)生在運(yùn)行期的,而且JVM中泛型的類型信息會(huì)被擦掉(跟Java里范型一樣不能匹配)。
case m: Map[String, Int] => ... // 不行,類型不起作用
case m: Map[_, _] => ... // 匹配通用的Map,OK
但對(duì)于數(shù)組來(lái)說(shuō),類型信息是完好的,所以可以在Array上匹配。
對(duì)于嵌套結(jié)構(gòu),舉例就能一目了然。
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這個(gè)變量被綁定到第一個(gè)Article的description。另外還可以使用@來(lái)將值綁定到變量:
// art被綁定為第一個(gè)Article,rest是剩余的Item序列
case Bundle(_, _, art @ Article(_, _), rest @ _*) => ...
樣例類
樣例類是種特殊的類,經(jīng)過(guò)優(yōu)化以用于模式匹配。
abstract class Amount
// 繼承了普通類的兩個(gè)樣例類
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount
// 樣例對(duì)象
case object Nothing extends Amount
使用:
amt match {
case Dollar(v) => "$" + v
case Currency(_, u) => "Oh noes, I got " + u
case Nothing => "" // 樣例對(duì)象沒(méi)有()
}
在聲明樣例類時(shí),下面的過(guò)程自動(dòng)發(fā)生了:
構(gòu)造器的每個(gè)參數(shù)都成為val,除非顯式被聲明為var,但是并不推薦這么做;
在伴生對(duì)象中提供了apply方法,所以可以不使用new關(guān)鍵字就可構(gòu)建對(duì)象;
提供unapply方法使模式匹配可以工作;
生成toString、equals、hashCode和copy方法,除非顯示給出這些方法的定義。
除了上述之外,樣例類和其他類型完全一樣,方法字段等。
密封類
當(dāng)使用樣例類來(lái)做模式匹配時(shí),如果要讓編譯器確保已經(jīng)列出所有可能的選擇,可以將樣例類的通用超類聲明為sealed。
密封類的所有子類都必須在與該密封類相同的文件中定義。
如果某個(gè)類是密封的,那么在編譯期所有的子類是可知的,因而可以檢查模式語(yǔ)句的完整性。
讓所有同一組的樣例類都擴(kuò)展某個(gè)密封的類或特質(zhì)是個(gè)好的做法。
模式匹配的目的
那么,為什么你需要模式匹配?我們每個(gè)人都有復(fù)雜的數(shù)據(jù)。如果我們堅(jiān)持嚴(yán)格的面向?qū)ο蟮娘L(fēng)格,那么我們并不希望直接訪問(wèn)數(shù)據(jù)內(nèi)部的樹(shù)狀結(jié)構(gòu)。相反,我們希望調(diào)用方法,然后在方法中訪問(wèn)。如果我們能夠這樣做,那么我們就再也不需要模式匹配了,因?yàn)檫@些方法已經(jīng)提供了我們需要的功能。但很多情況下,對(duì)象并不提供我們需要的方法,而且我們無(wú)法(或者不愿)向這些對(duì)象添加方法。
例如XML。如果給你一棵XML樹(shù),那么樹(shù)就只是單純的數(shù)據(jù)。要么是節(jié)點(diǎn),要么是節(jié)點(diǎn)的序列。XML是一種非常通用的數(shù)據(jù)表現(xiàn)形式。例如,DOM本質(zhì)上只是節(jié)點(diǎn)的數(shù)組,其中每個(gè)節(jié)點(diǎn)的類型都未知?,F(xiàn)在我們?cè)O(shè)想一下,如果把XML樹(shù)轉(zhuǎn)換到某種更強(qiáng)的框架中,可以給你一個(gè)列表,容納各種不同類型的對(duì)象。組成列表的元素可能包括諸如電話號(hào)碼、備忘錄或地址等。如果你想以靜態(tài)類型的方式獲取所有這些東西,就會(huì)遇上一個(gè)問(wèn)題:你不知道每個(gè)元素的類型。在傳統(tǒng)面向?qū)ο蟮木幊陶Z(yǔ)言中,唯一可行方式是,編寫一大堆instanceof檢測(cè),一一測(cè)試每個(gè)元素是PhoneNumber實(shí)例、Memo實(shí)例,還是其他實(shí)例。一旦這些instanceof語(yǔ)句之一檢測(cè)成功,你還需要進(jìn)行類型轉(zhuǎn)換。上述做法相當(dāng)丑陋和笨拙,有了模式匹配就能避免了。模式匹配能以更安全、更自然的方式完成相同功能。
從本質(zhì)上講,當(dāng)你從外部取得具有結(jié)構(gòu)的對(duì)象圖時(shí),模式匹配就必不可少。你會(huì)在若干情況下遇到這種現(xiàn)象,XML是其中之一。各種從文本解析而來(lái)的數(shù)據(jù),都屬于這一類。例如,有一種典型情況下模式匹配必不可少,即,處理編譯器中的抽象語(yǔ)法樹(shù)的情況。如果你要對(duì)表達(dá)式進(jìn)行化簡(jiǎn)操作,表達(dá)式會(huì)被表示為樹(shù),你需要通過(guò)模式匹配對(duì)這些樹(shù)進(jìn)行提取操作。類似那樣的情況還有許多。遇到這些情況時(shí),模式匹配真的必不可少。