前兩章花費(fèi)了相當(dāng)多的時(shí)間去解釋下面這兩件事情:
現(xiàn)在是時(shí)候去了解模式更多的用法了。
模式可能出現(xiàn)的一個(gè)地方就是 模式匹配表達(dá)式(pattern matching expression):
一個(gè)表達(dá)式 e
,后面跟著關(guān)鍵字 match
以及一個(gè)代碼塊,這個(gè)代碼塊包含了一些匹配樣例;
而樣例又包含了 case
關(guān)鍵字、模式、可選的 守衛(wèi)分句(guard clause) ,以及最右邊的代碼塊;
如果模式匹配成功,這個(gè)代碼塊就會(huì)執(zhí)行。
寫成代碼,看起來會(huì)是下面這種樣子:
e match {
case Pattern1 => block1
case Pattern2 if-clause => block2
...
}
下面是一個(gè)更具體的例子:
case class Player(name: String, score: Int)
def printMessage(player: Player) = player match {
case Player(_, score) if score > 100000 =>
println("Get a job, dude!")
case Player(name, _) =>
println("Hey, $name, nice to see you again!")
}
printMessage
的返回值類型是 Unit
,其唯一目的是執(zhí)行一個(gè)副作用,即打印一條信息。
要記住你不一定非要使用模式匹配,因?yàn)槟阋部梢允褂孟?Java 語言中的 switch 語句。
但這里使用的模式匹配表達(dá)式之所以叫 模式匹配表達(dá)式 是有原因的: 其返回值是由第一個(gè)匹配的模式中的代碼塊決定的。
使用它通常是好的,因?yàn)樗试S你解耦兩個(gè)并不真正屬于彼此的東西,也使得你的代碼更易于測試。 可把上面的例子重寫成下面這樣:
case class Player(name: String, score: Int)
def message(player: Player) = player match {
case Player(_, score) if score > 100000 =>
"Get a job, dude!"
case Player(name, _) =>
"Hey, $name, nice to see you again!"
}
def printMessage(player: Player) = println(message(player))
現(xiàn)在,獨(dú)立出一個(gè)返回值是 String
類型的 message
函數(shù),
它是一個(gè)純函數(shù),沒有任何副作用,返回模式匹配表達(dá)式的結(jié)果,
你可以將其保存為值,或者賦值給一個(gè)變量。
模式還可能出現(xiàn)值定義的左邊。 (以及變量定義,本書中變量的使用并不多,因?yàn)槲移蛴谑褂煤瘮?shù)式風(fēng)格的Scala代碼)
假設(shè)有一個(gè)方法,返回當(dāng)前的球員,我們可以模擬這個(gè)方法,讓它始終返回同一個(gè)球員:
def currentPlayer(): Player = Player("Daniel", 3500)
通常的值定義如下所示:
val player = currentPlayer()
doSomethingWithName(player.name)
如果你知道 Python,你可能會(huì)了解一個(gè)稱為 序列解包(sequence unpacking) 的功能, 它允許在值定義(或者變量定義)的左側(cè)使用模式。 你可以用類似的風(fēng)格編寫你的 Scala 代碼:改變我們的代碼,在將球員賦值給左側(cè)變量的同時(shí)去解構(gòu)它:
val Player(name, _) = currentPlayer()
doSomethingWithName(name)
你可以用任何模式來做這件事情,但得確保模式總能夠匹配,否則,代碼會(huì)在運(yùn)行時(shí)出錯(cuò)。
下面的代碼就是有問題的: scores
方法返回球員得分的列表。
為了說明問題,代碼中只是返回一個(gè)空的列表。
def scores: List[Int] = List()
val best :: rest = scores
println("The score of our champion is " + best)
運(yùn)行的時(shí)候,就會(huì)出現(xiàn) MatchError
。(好像我們的游戲不是那么成功,畢竟沒有任何得分)
一種安全且非常方便的使用方式是只解構(gòu)那些在編譯期就知道類型的樣例類。
此外,以這種方式來使用元組,代碼可讀性會(huì)更強(qiáng)。
假設(shè)有一個(gè)函數(shù),返回一個(gè)包含球員名字及其得分的元組,而不是先前定義的 Player
:
def gameResult(): (String, Int) = ("Daniel", 3500)
訪問元組字段的代碼給人感覺總是很怪異:
val result = gameResult()
println(result._1 + ": " + result._2)
這樣,在賦值的同時(shí)去解構(gòu)它是非常安全的,因?yàn)槲覀冎浪愋褪?Tuple2
:
val (name, score) = gameResult()
println(name + ": " + score)
這就好看多了,不是嗎?
模式在 for 語句中也非常重要。 所有能在值定義的左側(cè)使用的模式都適用于 for 語句的值定義。 因此,如果我們有一個(gè)球員得分集,想確定誰能進(jìn)名人堂(得分超過一定上限), 用 for 語句就可以解決:
def gameResults(): Seq[(String, Int)] =
("Daniel", 3500) :: ("Melissa", 13000) :: ("John", 7000) :: Nil
def hallOfFame = for {
result <- gameResults()
(name, score) = result
if (score > 5000)
} yield name
結(jié)果是 List("Melissa", "John")
,因?yàn)榈谝粋€(gè)球員得分沒超過 5000。
上面的代碼還可以寫的更簡單,for 語句中,生成器的左側(cè)也可以是模式。 從而,可以直接在左則把想要的值解構(gòu)出來:
def hallOfFame = for {
(name, score) <- gameResults()
if (score > 5000)
} yield name
模式 (name, score)
總會(huì)匹配成功,
如果沒有守衛(wèi)語句 if (score > 5000)
,
for 語句就相當(dāng)于直接將元組映射到球員名字,不會(huì)進(jìn)行過濾。
不過你要知道,生成器左側(cè)的模式也可以用來過濾。 如果左側(cè)的模式匹配失敗,那相關(guān)的元素就會(huì)被直接過濾掉。
為了說明這種情況,假設(shè)有一序列的序列,我們想返回所有非空序列的元素個(gè)數(shù)。 這就需要過濾掉所有的空列表,然后再返回剩下列表的元素個(gè)數(shù)。 下面是一個(gè)解決方案:
val lists = List(1, 2, 3) :: List.empty :: List(5, 3) :: Nil
for {
list @ head :: _ <- lists
} yield list.size
上面例子中,左側(cè)的模式不匹配空列表。
這不會(huì)拋出 MatchError
,但對應(yīng)的空列表會(huì)被丟掉,因此得到的結(jié)果是 List(3, 2)
。
模式和 for 語句是一個(gè)很自然、很強(qiáng)大的結(jié)合。 用 Scala 工作一段時(shí)間后,你會(huì)發(fā)現(xiàn)經(jīng)常需要它。
這一章講述了模式的多種使用方式。
除此之外,模式還可以用于定義匿名函數(shù),
如果你試過用 catch
塊處理 Scala 中的異常,那你就見過模式的這個(gè)用法,
下一章會(huì)詳細(xì)描述。