無處不在的模式

2018-02-24 16:00 更新

前兩章花費了相當多的時間去解釋下面這兩件事情:

  1. 用模式解構對象是怎么一回事。
  2. 如何構造自己的提取器。

現在是時候去了解模式更多的用法了。

模式匹配表達式

模式可能出現的一個地方就是 模式匹配表達式(pattern matching expression):一個表達式 e ,后面跟著關鍵字 match 以及一個代碼塊,這個代碼塊包含了一些匹配樣例;而樣例又包含了 case 關鍵字、模式、可選的 守衛(wèi)分句(guard clause) ,以及最右邊的代碼塊;如果模式匹配成功,這個代碼塊就會執(zhí)行。寫成代碼,看起來會是下面這種樣子:

e match {
  case Pattern1 => block1
  case Pattern2 if-clause => block2
  ...
}

下面是一個更具體的例子:

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í)行一個副作用,即打印一條信息。要記住你不一定非要使用模式匹配,因為你也可以使用像 Java 語言中的 switch 語句。

但這里使用的模式匹配表達式之所以叫 模式匹配表達式 是有原因的:其返回值是由第一個匹配的模式中的代碼塊決定的。

使用它通常是好的,因為它允許你解耦兩個并不真正屬于彼此的東西,也使得你的代碼更易于測試??砂焉厦娴睦又貙懗上旅孢@樣:

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))

現在,獨立出一個返回值是 String 類型的 message 函數,它是一個純函數,沒有任何副作用,返回模式匹配表達式的結果,你可以將其保存為值,或者賦值給一個變量。

值定義中的模式

模式還可能出現值定義的左邊。(以及變量定義,本書中變量的使用并不多,因為我偏向于使用函數式風格的Scala代碼)

假設有一個方法,返回當前的球員,我們可以模擬這個方法,讓它始終返回同一個球員:

def currentPlayer(): Player = Player("Daniel", 3500)

通常的值定義如下所示:

val player = currentPlayer()
doSomethingWithName(player.name)

如果你知道 Python,你可能會了解一個稱為 序列解包(sequence unpacking) 的功能,它允許在值定義(或者變量定義)的左側使用模式。你可以用類似的風格編寫你的 Scala 代碼:改變我們的代碼,在將球員賦值給左側變量的同時去解構它:

val Player(name, _) = currentPlayer()
doSomethingWithName(name)

你可以用任何模式來做這件事情,但得確保模式總能夠匹配,否則,代碼會在運行時出錯。下面的代碼就是有問題的: scores 方法返回球員得分的列表。為了說明問題,代碼中只是返回一個空的列表。

def scores: List[Int] = List()
val best :: rest = scores
println("The score of our champion is " + best)

運行的時候,就會出現 MatchError 。(好像我們的游戲不是那么成功,畢竟沒有任何得分)

一種安全且非常方便的使用方式是只解構那些在編譯期就知道類型的樣例類。此外,以這種方式來使用元組,代碼可讀性會更強。假設有一個函數,返回一個包含球員名字及其得分的元組,而不是先前定義的 Player

def gameResult(): (String, Int) = ("Daniel", 3500)

訪問元組字段的代碼給人感覺總是很怪異:

val result = gameResult()
println(result._1 + ": " + result._2)

這樣,在賦值的同時去解構它是非常安全的,因為我們知道它類型是 Tuple2

val (name, score) = gameResult()
println(name + ": " + score)

這就好看多了,不是嗎?

for 語句中的模式

模式在 for 語句中也非常重要。所有能在值定義的左側使用的模式都適用于 for 語句的值定義。因此,如果我們有一個球員得分集,想確定誰能進名人堂(得分超過一定上限),用 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

結果是 List("Melissa", "John") ,因為第一個球員得分沒超過 5000。

上面的代碼還可以寫的更簡單,for 語句中,生成器的左側也可以是模式。從而,可以直接在左則把想要的值解構出來:

def hallOfFame = for {
    (name, score) <- gameResults()
    if (score > 5000)
  } yield name

模式 (name, score) 總會匹配成功,如果沒有守衛(wèi)語句 if (score > 5000) ,for 語句就相當于直接將元組映射到球員名字,不會進行過濾。

不過你要知道,生成器左側的模式也可以用來過濾。如果左側的模式匹配失敗,那相關的元素就會被直接過濾掉。

為了說明這種情況,假設有一序列的序列,我們想返回所有非空序列的元素個數。這就需要過濾掉所有的空列表,然后再返回剩下列表的元素個數。下面是一個解決方案:

val lists = List(1, 2, 3) :: List.empty :: List(5, 3) :: Nil
for {
  list @ head :: _ <- lists
} yield list.size

上面例子中,左側的模式不匹配空列表。這不會拋出 MatchError ,但對應的空列表會被丟掉,因此得到的結果是 List(3, 2) 。

模式和 for 語句是一個很自然、很強大的結合。用 Scala 工作一段時間后,你會發(fā)現經常需要它。

小結

這一章講述了模式的多種使用方式。除此之外,模式還可以用于定義匿名函數,如果你試過用 catch 塊處理 Scala 中的異常,那你就見過模式的這個用法,下一章會詳細描述。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號