快學Scala第14章----模式匹配和樣例類

本章要點

  • match表達式是一個更好的switch,不會有意外掉入到下一個分支的問題。
  • 如果沒有模式能夠匹配,會拋出MatchError。可以用case _ 模式來避免。
  • 模式可以包含一個隨意定義的條件,稱作守衛。
  • 你可以對表達式的類型進行匹配;優先選擇模式匹配而不是isInstanceOf/asInstanceOf。
  • 你可以匹配數組、元組和樣例類的模式,然后將匹配到的不同部分綁定到變量。
  • 在for表達式中,不能匹配的情況會被安靜的跳過。
  • 樣例類繼承層級中的公共超類應該是sealed的。
  • 用Option來存放對于可能存在也可能不存在的值----這比null更安全。

更好的switch

以下是Scala中C風格switch語句的等效代碼:

var sign = ...
val ch: Char = ...

ch match {
  case '+' => sign = 1
  case '-' => sign = -1
  case _ => sign = 0
}

在這里,case _ 與 C 語言的 default 相同,可以匹配任意的模式,所以要注意放在最后。C 語言的 switch中的case語句必須使用break才能推出當前的分支,否則會繼續執行后面的分支,直到遇到break或者結束; 而Scala的模式匹配只會匹配到一個分支,不需要使用break語句,因為它不會掉入到下一個分支。
match是表達式,與if一樣,是有值的:

sign = ch match {
  case '+' => 1
  case '-' => -1
  case _ => 0
}

守衛

在C語言中,如果你想用switch判斷字符是數字,則必須這么寫:

switch(ch) {
  case '0':
  case '1':
  case '2':
  case '3':
  ...
  case '8':
  case '9': do something; break;
  default: ...; 
}

你要寫10條case語句才可以匹配所有的數字;而在Scala中,你只需要給模式添加守衛:

ch match {
  case '+' => 1
  case '-' => -1
  case _ if Character.isDigit(ch) => digit = Character.digit(ch, 10)
  case _ => 0
}

模式匹配中的變量

如果case關鍵字后面跟著一個變量名,那么匹配的表達式會被賦值給那個變量。

str(i) match {
  case '+' => 1
  case '-' => -1
  case ch => digit = Character.digit(ch, 10)
}

// 在守衛中使用變量
str(i) match {
  case ch if Character.isDigit(ch) => digit = Character.digit(ch, 10)
  ...
}

**注意: **Scala是如何在模式匹配中區分模式是常量還是變量表達式: 規則是變量必須是以小寫字母開頭的。 如果你想使用小寫字母開頭的常量,則需要將它包在反單引號中。


changliang.png

類型模式

你可以對表達式的類型進行匹配,例如:

obj match {
  case x: Int => x
  case s: String => Integer.parseInt(s)
  case _: BigInt => Int.MaxValue
  case - => 0
}

在Scala中我們會優先選擇模式匹配而不是isInstanceOf/asInstanceOf。
**注意: **當你在匹配類型的時候,必須給出一個變量名,否則你將會拿對象本身來進行匹配:

obj match {
  case _: BigInt => Int.MaxValue  // 匹配任何類型為BigInt的對象
  case BigInt => -1              // 匹配類型為Class的BigInt對象
}

**注意: **匹配發生在運行期,Java虛擬機中泛型的類型信息是被擦掉的。因此,你不能用類型來匹配特定的Map類型。

case m: Map[String, Int] => ...   // error
// 可以匹配一個通用的映射
case m: Map[_, _] => ...   // OK

// 但是數組作為特殊情況,它的類型信息是完好的,可以匹配到Array[Int]
case m: Array[Int] => ...   // OK

匹配數組、列表和元組

要匹配數組的內容,可以在模式中使用Array表達式:

arr match {
  case Array(0) => "0"                  // 任何包含0的數組
  case Array(x, y) => x + " " + y   // 任何只有兩個元素的數組,并將兩個元素本別綁定到變量x 和 y
  case Array(0, _*) => "0 ..."         // 任何以0開始的數組
  case _ => "Something else"
}

同樣也可以應用到List

lst match {
  case 0 :: Nil => "0"
  case x :: y :: Nil => x + " " + y
  case 0 :: tail => "0 ..."
  case _ => "Something else"
}

對于元組:

pair match {
  case (0, _) => "0, ..."
  case (y, 0) => y + " 0"
  case _ => "neither is 0"
}

提取器

在上面的模式是如何匹配數組、列表、元組的呢?Scala是使用了提取器機制----帶有從對象中提取值的unapply 或 unapplySeq方法的對象。其中, unapply方法用于提取固定數量的對象;而unapplySeq提取的是一個序列,可長可短。

arr match {
  case Array(0, x) => ...  // 匹配有兩個元素的數組,其中第一個元素是0,第二個綁定給x
}

Array伴生對象就是一個提取器----它定義了一個unapplySeq方法。該方法執行時為:Array.unapplySeq(arr) 產出一個序列的值。第一個值于0進行比較,第二個賦值給x。
正則表達式也可以用于提取器的場景。如果正則表達式有分組,可以用模式提取器來匹配每個分組:

val pattern = "([0-9]+) ([a-z]+)".r
"99 bottles" match {
  case pattern(num, item) => ...   // 將num設為99, item設為"bottles"
}

注意: 在這里提取器并不是一個伴生對象,而是一個正則表達式對象。


變量聲明中的模式

在變量聲明中也可以使用變量的模式匹配:

val (x, y) = (1, 2)  // 把x定義為1, 把y定義為2.
val (q, r) = BigInt(10) /% 3   // 匹配返回對偶的函數

// 匹配任何帶有變量的模式
val Array(first, second, _*)  = arr  

for表達式中的模式

你可以在for推導式中使用帶變量的模式。

import scala.collection.JavaConversions.propertiesAsScalaMap
for ((k, v) <- system.getProperties()) {
  println(k + " -> " + v)
}

在for推導式中,失敗的匹配將被安靜的忽略。例如:

// 只匹配值為空的情況
for ((k, "") <- system.getProperties()) {
  println(k)
}

// 也可以使用守衛
for ((k, v) <- system.getProperties() if v == "") {
  println(k)
}

樣例類

樣例類是一種特殊的類,它們經過優化以被用于模式匹配。

abstract class Amount
case class Dollar(value; Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount

// 針對單例的樣例對象
case object Nothing extends Amount

// 將Amount類型的對象用模式匹配來匹配到它的類型,并將屬性值綁定到變量:
amt match {
  case Dollar(v) => "$" + v
  case Currency(_, u) => "Oh noes, I got " + u
  case Nothing => ""
}

當你聲明樣例類時,如下事情會自動發生:

  • 構造器中每一個參數都成為val----除非它被顯示的聲明為var(不建議這樣做)
  • 在伴生對象中提供apply方法讓你不用new關鍵字就能夠構造出相應的對象,例如Dollar(2)或Currency(34, "EUR")
  • 提供unapply方法讓模式匹配可以工作
  • 將生成toString、equals、hashCode和copy方法----除非你顯示的給出這些方法的定義。

copy方法和帶名參數

樣例類的copy方法創建一個與現有對象值相同的新對象。例如:

val amt = Currency(29.95, "EUR")
val price = amy.copy()    // Currency(29.95, "EUR")
val price2 = amt.copy(value = 19.95)  // Currency(19.95, "EUR")
val price3 = amt.copy(unit = "CHF")    // Currency(29.95, "CHF")

case語句中的中置表示法

如果unapply方法產出一個對偶,則可以在case語句中使用中置表示法。尤其是對于兩個參數的樣例類,你可以使用中置表示法來表示它。

amt match { case a Currency u => ... }  // 等同于 case Currency(a, u)

這個特性的本意是要匹配序列。例如:每個List對象要么是Nil,要么是樣例類::, 定義如下:

case class ::[E](head: E, tail: List[E]) extends List[E]
// 因此你可以這么寫
lst match {
  case h :: t => ...   // 等同于 case ::(h, t), 將調用::.unapply(lst)
}

匹配嵌套結構

樣例類經常被用于嵌套結構。例如:商店售賣的商品:

abstract class Item
case class Article(description: String, price: Double) extends Item
case class Bundle(description: String, discount: 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綁定到Bundle的第一個Article的描述。你也可以@表示法將嵌套的值綁定到變量:

case Bundle(_, _, art @ Article(_, _), rest @ _*) => ...

這樣,art就是Bundle中的第一個Article, 而rest則是剩余Item的序列。 _*代表剩余的Item。
該特性實際應用:

def price(it: Item): Double = it match {
  case Article(_, p) => p
  case Bundle(_, disc, its @ _*) => its.map(price _).sum - disc
}

樣例類是邪惡的嗎

樣例類適用于那種標記了不會改變的結構。例如Scala的List就是用樣例類實現的。

abstract class List
case object Nil extends List
case class ::(head: Any, tail: List) extends List

當用在合適的地方時,樣例類是十分便捷的,原因如下:

  • 模式匹配通常比繼承更容易把我們引向更精簡的代碼。
  • 構造時不需要用new的符合對象更加易讀
  • 你將免費獲得toString、equals、hashCode和copy方法。
    對于樣例類:
case class Currency(value: Double, unit: String)

一個Currency(10, "EUR")和任何其他Currency(10, "EUR")都是等效的,這也是equals和hashCode方法實現的依據。這樣的類通常都是不可變的。對于那些帶有可變字段的樣例類,我們總是從那些不會改變的字段來計算和得出其哈希值,比如用ID字段。


密封類

密封類是指用sealed修飾的類。密封類的所有子類都必須在與該密封類相同的文件中定義。這樣做的好處是:當你用樣例類來做模式匹配時,你可以讓編譯器確保你已經列出了所有可能的選擇,編譯器可以檢查模式語句的完整性。

sealed abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amunt

上述的樣例類必須與Amount類在一個文件中。


模擬枚舉

sealed abstract class TrafficLightColor
case object Red extends TrafficLightColor
case object Yellow extends TrafficLightColor
case object Green extends TrafficLightColor

color match {
  case Red => "stop"
  case Yellow => "hurry up"
  case Green => "go"
}

Option類型

標準庫中的Option類型用樣例類來表示那種可能存在、也可能不存在的值。樣例子類Some包裝了某個值,例如: Some("Fred"). 而樣例對象None表示沒有值。這比使用空字符串的意圖更加清晰,比使用null來表示缺少的值的做法更安全。
Option支持泛型,例如:Some("Fred") 的類型是Option[String]。
Map的get方法返回一個Option。如果對于給定的鍵沒有對應的值,則get返回None,如果有值,就會將該值包在Some中返回。

scores.get("Alice") match {
  case Some(score) => println(score)
  case None => println("No score")
}

//  可以使用isEmpty 和 get 替代上面代碼
val aliceScore = scores.get("Alice")
if (aliceScore.isEmpty) println("No score")
else println(aliceScore.get)

// 使用更簡便的 getOrElse方法
println(aliceScore.getOrElse("No score"))

偏函數

被包在花括號內的一組case語句是一個偏函數----一個并非對所有輸入值都有定義的函數。它是PartialFunction[A, B]類的一個實例。其中A是參數類型,B是返回類型。該類有兩個方法:apply從匹配到的模式計算函數值, 而isDefinedAt方法在輸入至少匹配其中一個模式時返回true。

val f: PartialFunction[Char, Int] = { case '+' => 1; case '-' => -1 }
f('-')   // 調用 f.apply('-'), 返回-1
f.isDefinedAt('0')  // fase
f('0')  // 拋出MatchError

有一些方法接受PartialFunction作為參數。例如 GenTraversable特質的collect方法將一個偏函數應用到所有該偏函數有定義的元素,并返回包含這些結果的序列:

"-3+4".collect {case '+' => 1;  case '-' => -1 }  // Vector(-1, 1)
PartialFun.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容