Scala學習(十四)模式匹配和樣例類

1.更好的switch

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

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

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

在這裏,case _ 與 C 語言的 default 相同,能夠匹配任意的模式,因此要注意放在最後。有這樣一個能捕獲全部模式是有好處的。若是沒有模式可以匹配,代碼會拋出MatchError。正則表達式

C 語言的 switch中的case語句必須使用break才能推出當前的分支,不然會繼續執行後面的分支,直到遇到break或者結束; 而Scala的模式匹配只會匹配到一個分支,不須要使用break語句,由於它不會掉入到下一個分支。數組

match是表達式,與if同樣,是有值的:安全

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

用|來分隔多個選項:app

prefix match{
    case "0" | "0x" | "0X" => ...
    ...
}

你能夠在match表達式中使用任何類型,而不只僅是數字。函數

2.守衛

在C語言中,若是你想用switch判斷字符是數字,則必須這麼寫:優化

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

你要寫10條case語句才能夠匹配全部的數字;而在Scala中,你只須要給模式添加守衛:spa

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

模式老是自上而下進行匹配。scala

3.模式中的變量

若是case關鍵字後面跟着一個變量名,那麼匹配的表達式會被賦值給那個變量。rest

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是如何在模式匹配中區分模式是常量仍是變量表達式: 規則是變量必須是以小寫字母開頭的。 若是你想使用小寫字母開頭的常量,則須要將它包在反單引號中。

4.類型模式

你能夠對錶達式的類型進行匹配,例如:

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

此時obj對象的類型必須是模式匹配中全部類型公共的超類,不然報錯。

在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

5.匹配數組、列表和元組

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

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

若是你想講匹配到 _* 的變長度參數綁定到變量,你能夠用 @ 表示法,就像這樣:

case Array(x,rest @ _*) => rest.min

一樣也能夠應用到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"
}

說明:若是模式有不一樣的可選分支,你就不能使用除下劃線外的其餘變量命名。

pair match{
   case (_,0) | (0,_) => ... //ok 若是其中一個是0
   case (x,0) | (0,x) => ... //錯誤——不能對可選分支作變量綁定
}

6.提取器

在上面的模式是如何匹配數組、列表、元組的呢?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"
}

pattern.unapplySeq("99",bottles)交出的是一系列匹配分組的字符創。這些字符串被分別賦值給了num和item。

注意: 在這裏提取器並非一個伴生對象,而是一個正則表達式對象。

7.變量聲明模式

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

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

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

上述代碼將數組arr的第一個和第二個元素分別賦值給了first和second,並將剩餘的元素做爲一個Seq複製給了rest

8.for表達式中的模式

你能夠在for推導式中使用帶變量的模式。

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

對應映射每個(鍵,值)對偶,k被綁定到鍵,而v被綁定到值。

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

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

你也可使用守衛。注意if關鍵字出如今 <- 以後。

for ((k, v) <- system.getProperties() if v == "") {
    println(k)
}

9.樣例類

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

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方法----除非你顯示的給出這些方法的定義。

除了上述節點外,樣例類和其餘類徹底同樣。你能夠添加方法和字段,擴展他們,等等。

10.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")

11.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)
}

說明:中置表示法用於任何返回對偶的unapply方法。如下是一個示例:

case object +: {

    def unapply[T](input: List[T]) = 

        if (input.isEmpty) None else Some((input.head, input.tail))

}

這樣一來你就能夠用+:來構析列表了

1 +: 7 +: 2 +: 9 +: Nil match{

    case first +: second +: rest => first + second + rest.length

}

12.嵌套匹配

樣例類常常被用於嵌套結構。例如,某個商店收買的物品。有時,咱們會將物品捆綁在一塊兒打折出售。

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。

該特性實際應用,如下是一個計算某Item價格的函數

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

13.樣例類是邪惡的嗎

樣例類適用於那種標記了不會改變的結構。例如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字段。

14.密封類

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

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

舉例來講,若是有人想要爲歐元添加另外一個樣例類:

case class Euro(value: Double) extends Amount

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

15.Option類型

標準類庫中的Option類型用樣例類來表示那種可能存在也可能不存在的值。樣例子類Some包裝了某個值,例如:Some("Fred")。而樣例對象None表示沒有值。

這筆使用空字符串的意圖更加清晰,比使用null來表示缺乏某值得作法更加安全。

Option支持泛型。舉例來講Some("Fred")的類型爲Option[String]。

Map類的get方法返回一個Option。若是對於給定的鍵沒有值,則get返回None。若是有值,就會將改值包裝在Some中返回。

你能夠用模式匹配來分析這樣一個值:

val p = scores.get("Alice")

p match{
    case Some(score) => println(score)
    case None => println("No score")
}

有點麻煩,你也可使用isEmpty和get:

if(p.isEmpty) println("No score") else println(p.get)

這也很麻煩。用getOrElse更好:

println(p.getOrElse("No score")) //若是p爲None,getOrElse將返回No score

處理可選值(Option)更強力的方式是將他們當作擁有0或1個元素的集合。你能夠用for循環來訪問這個元素:

for(score <- p) println(score)

若是p是None,則什麼都不會發生。若是他是一個Some,那麼循環將被執行,而sorce將會被綁上可選值內容。

你也能夠用諸如map、filter或foreach方法。例如:

val b = p.map( _ + 1)  //Some(score + 1) 或 None

val a = p.filter(_ > 5) //若是score > 5,則獲得Some(score ),不然獲得None

p.foreach(println _) //若是存在,打印score值

提示:在從一個可能爲null的值建立Option時,你可能簡單地使用Option(value)。若是value爲null,結果就是None;其他狀況獲得Some(value)。

16.偏函數

被包含在花括號內的一組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)

Seq[A]是一個PartialFunction[Int,A],而Map[K,V]是一個PartialFunction[K,V]。例如,你能夠將映射傳入collect:

val names = Array("Alice","Bob","Carmen")

var scores = Map("Alice"->10,"Carmen"→7)

names.collects(scores )  //將交出Array(10,7)

lift方法將PartialFunction[T,R]變成一個返回類型爲Option[R]的常規函數。

var f :PartialFunction[Char, Int] = {case '+' => 1;case '-' => -1}

var g = f.lift //一個類型爲Char => Option[Int]的函數

這樣一來,g('-')獲得Some(-1),而g('*')獲得None。

相反,你也能夠調用Function.unlift將Option[R]的函數變成一個偏函數。

說明:try語句的catch字句是一個偏函數。你甚至可使用一個持有函數的變量。

def tryCatch[T](b: => T, catcher: PartialFunction[Throwable, T]) = try {b} catch catcher

而後,你就能夠像以下這樣提供一個定製的catch子句:

val result = tryCatch(str.toInt, { case _: NumberFormatException => -1})
相關文章
相關標籤/搜索