快學(xué)Scala第14章----模式匹配和樣例類

本章要點(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ī)則是變量必須是以小寫字母開頭的射赛。 如果你想使用小寫字母開頭的常量,則需要將它包在反單引號中奶是。


changliang.png

類型模式

你可以對表達(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)
PartialFun.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末屁柏,一起剝皮案震驚了整個(gè)濱河市啦膜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌淌喻,老刑警劉巖僧家,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異裸删,居然都是意外死亡八拱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門涯塔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肌稻,“玉大人,你說我怎么就攤上這事匕荸〉破迹” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵每聪,是天一觀的道長旦棉。 經(jīng)常有香客問我,道長药薯,這世上最難降的妖魔是什么绑洛? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮童本,結(jié)果婚禮上真屯,老公的妹妹穿的比我還像新娘。我一直安慰自己穷娱,他們只是感情好绑蔫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著泵额,像睡著了一般配深。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嫁盲,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天篓叶,我揣著相機(jī)與錄音,去河邊找鬼羞秤。 笑死缸托,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瘾蛋。 我是一名探鬼主播俐镐,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼哺哼!你這毒婦竟也來了佩抹?” 一聲冷哼從身側(cè)響起奇唤,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匹摇,沒想到半個(gè)月后咬扇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡廊勃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年懈贺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坡垫。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡梭灿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冰悠,到底是詐尸還是另有隱情堡妒,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布溉卓,位于F島的核電站皮迟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏桑寨。R本人自食惡果不足惜伏尼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尉尾。 院中可真熱鬧爆阶,春花似錦、人聲如沸沙咏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肢藐。三九已至故河,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窖壕,已是汗流浹背忧勿。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工杉女, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瞻讽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓熏挎,卻偏偏與公主長得像速勇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子坎拐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容