本章要點
- 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是如何在模式匹配中區分模式是常量還是變量表達式: 規則是變量必須是以小寫字母開頭的。 如果你想使用小寫字母開頭的常量,則需要將它包在反單引號中。
類型模式
你可以對表達式的類型進行匹配,例如:
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)