本章要點(diǎn)
- match表達(dá)式是一個(gè)更好的switch染服,不會(huì)有意外掉入到下一個(gè)分支的問題懒浮。
- 如果沒有模式能夠匹配,會(huì)拋出MatchError榄笙⌒澳可以用case _ 模式來避免。
- 模式可以包含一個(gè)隨意定義的條件茅撞,稱作守衛(wèi)帆卓。
- 你可以對表達(dá)式的類型進(jìn)行匹配;優(yōu)先選擇模式匹配而不是isInstanceOf/asInstanceOf米丘。
- 你可以匹配數(shù)組鳞疲、元組和樣例類的模式,然后將匹配到的不同部分綁定到變量蠕蚜。
- 在for表達(dá)式中尚洽,不能匹配的情況會(huì)被安靜的跳過。
- 樣例類繼承層級中的公共超類應(yīng)該是sealed的靶累。
- 用Option來存放對于可能存在也可能不存在的值----這比null更安全腺毫。
更好的switch
以下是Scala中C風(fēng)格switch語句的等效代碼:
var sign = ...
val ch: Char = ...
ch match {
case '+' => sign = 1
case '-' => sign = -1
case _ => sign = 0
}
在這里,case _ 與 C 語言的 default 相同挣柬,可以匹配任意的模式潮酒,所以要注意放在最后。C 語言的 switch中的case語句必須使用break才能推出當(dāng)前的分支邪蛔,否則會(huì)繼續(xù)執(zhí)行后面的分支急黎,直到遇到break或者結(jié)束; 而Scala的模式匹配只會(huì)匹配到一個(gè)分支,不需要使用break語句勃教,因?yàn)樗粫?huì)掉入到下一個(gè)分支淤击。
match是表達(dá)式,與if一樣故源,是有值的:
sign = ch match {
case '+' => 1
case '-' => -1
case _ => 0
}
守衛(wèi)
在C語言中污抬,如果你想用switch判斷字符是數(shù)字,則必須這么寫:
switch(ch) {
case '0':
case '1':
case '2':
case '3':
...
case '8':
case '9': do something; break;
default: ...;
}
你要寫10條case語句才可以匹配所有的數(shù)字绳军;而在Scala中印机,你只需要給模式添加守衛(wèi):
ch match {
case '+' => 1
case '-' => -1
case _ if Character.isDigit(ch) => digit = Character.digit(ch, 10)
case _ => 0
}
模式匹配中的變量
如果case關(guān)鍵字后面跟著一個(gè)變量名,那么匹配的表達(dá)式會(huì)被賦值給那個(gè)變量门驾。
str(i) match {
case '+' => 1
case '-' => -1
case ch => digit = Character.digit(ch, 10)
}
// 在守衛(wèi)中使用變量
str(i) match {
case ch if Character.isDigit(ch) => digit = Character.digit(ch, 10)
...
}
**注意: **Scala是如何在模式匹配中區(qū)分模式是常量還是變量表達(dá)式: 規(guī)則是變量必須是以小寫字母開頭的射赛。 如果你想使用小寫字母開頭的常量,則需要將它包在反單引號中奶是。
類型模式
你可以對表達(dá)式的類型進(jìn)行匹配楣责,例如:
obj match {
case x: Int => x
case s: String => Integer.parseInt(s)
case _: BigInt => Int.MaxValue
case - => 0
}
在Scala中我們會(huì)優(yōu)先選擇模式匹配而不是isInstanceOf/asInstanceOf。
**注意: **當(dāng)你在匹配類型的時(shí)候诫隅,必須給出一個(gè)變量名腐魂,否則你將會(huì)拿對象本身來進(jìn)行匹配:
obj match {
case _: BigInt => Int.MaxValue // 匹配任何類型為BigInt的對象
case BigInt => -1 // 匹配類型為Class的BigInt對象
}
**注意: **匹配發(fā)生在運(yùn)行期帐偎,Java虛擬機(jī)中泛型的類型信息是被擦掉的逐纬。因此,你不能用類型來匹配特定的Map類型削樊。
case m: Map[String, Int] => ... // error
// 可以匹配一個(gè)通用的映射
case m: Map[_, _] => ... // OK
// 但是數(shù)組作為特殊情況豁生,它的類型信息是完好的,可以匹配到Array[Int]
case m: Array[Int] => ... // OK
匹配數(shù)組漫贞、列表和元組
要匹配數(shù)組的內(nèi)容甸箱,可以在模式中使用Array表達(dá)式:
arr match {
case Array(0) => "0" // 任何包含0的數(shù)組
case Array(x, y) => x + " " + y // 任何只有兩個(gè)元素的數(shù)組,并將兩個(gè)元素本別綁定到變量x 和 y
case Array(0, _*) => "0 ..." // 任何以0開始的數(shù)組
case _ => "Something else"
}
同樣也可以應(yīng)用到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"
}
提取器
在上面的模式是如何匹配數(shù)組迅脐、列表芍殖、元組的呢?Scala是使用了提取器機(jī)制----帶有從對象中提取值的unapply 或 unapplySeq方法的對象谴蔑。其中豌骏, unapply方法用于提取固定數(shù)量的對象;而unapplySeq提取的是一個(gè)序列隐锭,可長可短窃躲。
arr match {
case Array(0, x) => ... // 匹配有兩個(gè)元素的數(shù)組,其中第一個(gè)元素是0钦睡,第二個(gè)綁定給x
}
Array伴生對象就是一個(gè)提取器----它定義了一個(gè)unapplySeq方法蒂窒。該方法執(zhí)行時(shí)為:Array.unapplySeq(arr) 產(chǎn)出一個(gè)序列的值。第一個(gè)值于0進(jìn)行比較,第二個(gè)賦值給x洒琢。
正則表達(dá)式也可以用于提取器的場景秧秉。如果正則表達(dá)式有分組,可以用模式提取器來匹配每個(gè)分組:
val pattern = "([0-9]+) ([a-z]+)".r
"99 bottles" match {
case pattern(num, item) => ... // 將num設(shè)為99纬凤, item設(shè)為"bottles"
}
注意: 在這里提取器并不是一個(gè)伴生對象福贞,而是一個(gè)正則表達(dá)式對象。
變量聲明中的模式
在變量聲明中也可以使用變量的模式匹配:
val (x, y) = (1, 2) // 把x定義為1停士, 把y定義為2.
val (q, r) = BigInt(10) /% 3 // 匹配返回對偶的函數(shù)
// 匹配任何帶有變量的模式
val Array(first, second, _*) = arr
for表達(dá)式中的模式
你可以在for推導(dǎo)式中使用帶變量的模式挖帘。
import scala.collection.JavaConversions.propertiesAsScalaMap
for ((k, v) <- system.getProperties()) {
println(k + " -> " + v)
}
在for推導(dǎo)式中,失敗的匹配將被安靜的忽略恋技。例如:
// 只匹配值為空的情況
for ((k, "") <- system.getProperties()) {
println(k)
}
// 也可以使用守衛(wèi)
for ((k, v) <- system.getProperties() if v == "") {
println(k)
}
樣例類
樣例類是一種特殊的類拇舀,它們經(jīng)過優(yōu)化以被用于模式匹配。
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 => ""
}
當(dāng)你聲明樣例類時(shí)骄崩,如下事情會(huì)自動(dòng)發(fā)生:
- 構(gòu)造器中每一個(gè)參數(shù)都成為val----除非它被顯示的聲明為var(不建議這樣做)
- 在伴生對象中提供apply方法讓你不用new關(guān)鍵字就能夠構(gòu)造出相應(yīng)的對象,例如Dollar(2)或Currency(34, "EUR")
- 提供unapply方法讓模式匹配可以工作
- 將生成toString薄辅、equals要拂、hashCode和copy方法----除非你顯示的給出這些方法的定義。
copy方法和帶名參數(shù)
樣例類的copy方法創(chuàng)建一個(gè)與現(xiàn)有對象值相同的新對象站楚。例如:
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方法產(chǎn)出一個(gè)對偶脱惰,則可以在case語句中使用中置表示法。尤其是對于兩個(gè)參數(shù)的樣例類窿春,你可以使用中置表示法來表示它拉一。
amt match { case a Currency u => ... } // 等同于 case Currency(a, u)
這個(gè)特性的本意是要匹配序列。例如:每個(gè)List對象要么是Nil旧乞,要么是樣例類::蔚润, 定義如下:
case class ::[E](head: E, tail: List[E]) extends List[E]
// 因此你可以這么寫
lst match {
case h :: t => ... // 等同于 case ::(h, t), 將調(diào)用::.unapply(lst)
}
匹配嵌套結(jié)構(gòu)
樣例類經(jīng)常被用于嵌套結(jié)構(gòu)尺栖。例如:商店售賣的商品:
abstract class Item
case class Article(description: String, price: Double) extends Item
case class Bundle(description: String, discount: Double, items: Item*) extends Item
// 產(chǎn)生嵌套對象
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的第一個(gè)Article的描述。你也可以@表示法將嵌套的值綁定到變量:
case Bundle(_, _, art @ Article(_, _), rest @ _*) => ...
這樣延赌,art就是Bundle中的第一個(gè)Article除盏, 而rest則是剩余Item的序列。 _*代表剩余的Item皮胡。
該特性實(shí)際應(yīng)用:
def price(it: Item): Double = it match {
case Article(_, p) => p
case Bundle(_, disc, its @ _*) => its.map(price _).sum - disc
}
樣例類是邪惡的嗎
樣例類適用于那種標(biāo)記了不會(huì)改變的結(jié)構(gòu)痴颊。例如Scala的List就是用樣例類實(shí)現(xiàn)的。
abstract class List
case object Nil extends List
case class ::(head: Any, tail: List) extends List
當(dāng)用在合適的地方時(shí)屡贺,樣例類是十分便捷的蠢棱,原因如下:
- 模式匹配通常比繼承更容易把我們引向更精簡的代碼锌杀。
- 構(gòu)造時(shí)不需要用new的符合對象更加易讀
- 你將免費(fèi)獲得toString、equals泻仙、hashCode和copy方法糕再。
對于樣例類:
case class Currency(value: Double, unit: String)
一個(gè)Currency(10, "EUR")和任何其他Currency(10, "EUR")都是等效的,這也是equals和hashCode方法實(shí)現(xiàn)的依據(jù)玉转。這樣的類通常都是不可變的突想。對于那些帶有可變字段的樣例類,我們總是從那些不會(huì)改變的字段來計(jì)算和得出其哈希值究抓,比如用ID字段猾担。
密封類
密封類是指用sealed修飾的類。密封類的所有子類都必須在與該密封類相同的文件中定義刺下。這樣做的好處是:當(dāng)你用樣例類來做模式匹配時(shí)绑嘹,你可以讓編譯器確保你已經(jīng)列出了所有可能的選擇,編譯器可以檢查模式語句的完整性橘茉。
sealed abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amunt
上述的樣例類必須與Amount類在一個(gè)文件中工腋。
模擬枚舉
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類型
標(biāo)準(zhǔn)庫中的Option類型用樣例類來表示那種可能存在、也可能不存在的值畅卓。樣例子類Some包裝了某個(gè)值擅腰,例如: Some("Fred"). 而樣例對象None表示沒有值。這比使用空字符串的意圖更加清晰翁潘,比使用null來表示缺少的值的做法更安全趁冈。
Option支持泛型,例如:Some("Fred") 的類型是Option[String]唐础。
Map的get方法返回一個(gè)Option箱歧。如果對于給定的鍵沒有對應(yīng)的值矾飞,則get返回None一膨,如果有值,就會(huì)將該值包在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"))
偏函數(shù)
被包在花括號內(nèi)的一組case語句是一個(gè)偏函數(shù)----一個(gè)并非對所有輸入值都有定義的函數(shù)豹绪。它是PartialFunction[A, B]類的一個(gè)實(shí)例。其中A是參數(shù)類型申眼,B是返回類型瞒津。該類有兩個(gè)方法:apply從匹配到的模式計(jì)算函數(shù)值, 而isDefinedAt方法在輸入至少匹配其中一個(gè)模式時(shí)返回true括尸。
val f: PartialFunction[Char, Int] = { case '+' => 1; case '-' => -1 }
f('-') // 調(diào)用 f.apply('-')巷蚪, 返回-1
f.isDefinedAt('0') // fase
f('0') // 拋出MatchError
有一些方法接受PartialFunction作為參數(shù)。例如 GenTraversable特質(zhì)的collect方法將一個(gè)偏函數(shù)應(yīng)用到所有該偏函數(shù)有定義的元素濒翻,并返回包含這些結(jié)果的序列:
"-3+4".collect {case '+' => 1; case '-' => -1 } // Vector(-1, 1)