1. 程式人生 > 實用技巧 >[Scala學習筆記]15-樣例類和模式匹配

[Scala學習筆記]15-樣例類和模式匹配

ch 15

樣例類是Scala用來對物件進行模式匹配而並不需要大量的樣板程式碼的方式。

本章內容:

  • 樣例類和模式匹配的例子
  • Scala支援的各種模式
  • 密封類(sealed class)
  • Option型別

15.1 一個簡單的例子

實現一個操作算術表示式的類庫:

// 定義輸入資料
abstract class Expr // 省去了空定義體的{}
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class Unop(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr

樣例類

帶有case修飾符的類。

  1. 新增一個跟類同名的工廠方法。可以用Var("x")構造物件。
  2. 引數列表中的引數都隱式獲得了一個val字首,它們會被當作欄位。如v.nameop.left等。
  3. 編譯器會幫助我們實現toStringhashCodeequals方法。
  4. 編譯器還會新增一個copy方法用於製作修改過的,除了一兩個屬性不同之外其餘完全相同的該類的新例項。例如:op.copy(operator = "-")。用帶名字的引數給出想要做的修改,沒有給出的引數將使用老物件中的原值。

模式匹配

假定我們想簡化前面的算術表示式。可用的簡化規則如下:

  • UnOp("-", UnOp("-", e)) => e // -(-x) => +x
  • BinOp("+", e, Number(0)) => e // x+0 => x
  • BinOp("*", e, Number(1)) => e // x*1 => x

使用模式匹配實現simplifyTop函式:

def simplifyTop(expr: Expr): Expr = expr match {
    case UnOp("-", UnOp("-", e)) => e
    case BinOp("+", e, Number(0)) => e
    case BinOp("*", e, Number(1)) => e
    case _ => expr
}

ScalamatchJavaswitch的區別:

  1. match是一個表示式(也就是說它總是能得到一個值)
  2. Scala的可選分支不會fall through到下一個case
  3. 如果沒有模式匹配上,會丟擲一個MatchError異常
expr match {
    case BinOp(op, left, right) =>
        println(expr + " is a binary operation")
    case _ => /* 什麼都不會發生 結果是 unit 值 */
}

15.2 模式的種類

通配模式

1. 用於預設、捕獲所有的可選路徑

case _ => // default case

2. 用於忽略某個物件中不關心的區域性

expr match {
    case BinOp(_, _, _) => println(expr + " is a binary operation")
    case _ => println("It's something else")
}

常量模式

僅匹配自己。任何字面量、val或單例物件都可以作為常量模式使用。例如5是常量,Nil是單例物件。

def describe(x: Any) = x match {
    case 5 => "five"
    case true => "truth"
    case "hello" => "hi!"
    case Nil => "the empty list"
    case _ => "something else"
}
describe(5)
describe(true)
describe("Hello!")

變數模式

匹配任何物件,Scala將對應的變數繫結成匹配上的物件。

expr match {
    case 0 => "zero"
    case somethingElse => "not zero: " + somethingElse
}

變數還是常量?

常量模式也可以有符號形式的名稱。

import math.{E, Pi}

E match {
    case Pi => "strange math? Pi = " + Pi
    case _ => "OK"
}

E並不匹配PiScala採用了一個簡單的詞法規則來區分:一個以小寫字母打頭的簡單名稱會被當作模式變數處理;所有其它引用都是常量。

val pi = math.Pi

E match {
    case pi => "strange math? Pi = " + pi
}

這裡編譯器不允許新增一個預設的case。由於pi是變數模式,它將會匹配所有的輸入,因此不可能走到後面的case

E match {
    case pi => "strange math? Pi = " + pi
    case _ => "OK"
}

如果需要,仍然可以用小寫名稱作為模式常量。

  1. 如果常量是某個物件的欄位,則可以在欄位名前面加上限定詞。例如this.piobj.pi
  2. 可以用反引號將這個名稱包起來。
E match {
    case `pi` => "strange math? Pi = " + pi
    case _ => "OK"
}

構造方法模式

例如BinOp("+", e, Number(0))

它由一個名稱(BinOp)和一組模式組成。假定這裡的名稱指定的是一個樣例類,這樣的一個模式將首先檢查被匹配的物件是否是以這個名稱命名的樣例類的例項,然後再檢查這個物件的構造方法引數是否匹配這些額外給出的模式。

expr match {
    case BinOp("+", e, Number(0)) => println("a deep match")
    case _ =>
}

序列模式

也可以和序列型別如ListArray匹配。

expr match {
    // 以0開始的3元素列表
    case List(0, _, _) => println("found it")
    case _ =>
}
expr match {
    // _*可以匹配序列中任意數量的元素
    case List(0, _*) => println("found it")
    case _ =>
}

元組模式

def tupleDemo(expr: Any) =
    expr match {
        case (a, b ,c) => println("matched " + a + b + c)
        case _ =>
    }

tupleDemo("a ", 3, "-tuple")
matched a 3-tuple

帶型別的模式

替代型別測試和型別轉換。

def generalSize(x: Any) = x match {
    case s: String => s.length
    case m: Map[_, _] => m.size
    case _ => -1
}

generalSize("abc")
generalSize(Map(1 -> 'a', 2 -> 'b'))
generalSize(math.Pi)

Scala的型別測試和轉換

  1. 型別測試:expr.isInstanceOf[String]
  2. 型別轉換:expr.asInstanceOf[String]
// 重寫case s: String => s.length這一表達式
if (x.isInstanceOf[String]) {
    val s = x.asInstanceOf[String]
    s.length
} else ...

型別擦除

測試某個值是否是IntInt型別的對映。

def isIntIntMap(x: Any) = x match {
    case m: Map[Int, Int] => true
    case _ => false
} // wrong

Scala採用了擦除式的泛型,就跟Java一樣。這意味著在執行時並不會保留型別引數的資訊。這麼一來,我們在執行時就無法判斷某個給定的Map物件是用兩個Int的型別引數建立的,還是其他什麼型別引數建立的。系統能做的只是判斷某個值是某種不確定型別引數的Map。可以把isintlntMap應用到不同的Map類例項來驗證這個行為:

isIntIntMap(Map(1 -> 1))
isIntIntMap(Map("abc" -> "abc"))

對於這個擦除規則唯一的例外是陣列,因為JavaScala都對它們做了特殊處理。陣列的元素型別是跟陣列一起儲存的,因此我們可以對它進行模式匹配。例如:

def isStringArray(x: Any) = x match {
    case a: Array[String] => "yes"
    case _ => "no"
}

val as = Array("abc")
isStringArray(as)

val ai = Array(1, 2, 3)
isStringArray(ai)

變數繫結

對模式新增變數。

expr match {
    // 若這個匹配成功了 則 e = UnOp("abs", _)
    case UnOp("abs", e @ UnOp("abs", _)) => e
    case _ =>
}

15.3 模式守衛

def simplifyAdd(e: Expr) = e match {
    case BinOp("+", x, y) if x == y => // 不能寫 BinOp("+", x, x)
        BinOp("*", x, Number(2))
    case _ => e
}
// 匹配正整數
case n: Int if 0 < n => ...

// 匹配'a'開頭的字串
case s: String if s(0) == 'a' => ...

15.4 模式重疊

模式會按照程式碼中的順序逐個被嘗試。

def simplifyAll(expr: Expr): Expr = expr match {
    case UnOp("-", UnOp("-", e)) =>
        simplifyAll(e)
    case BinOp("+", e, Number(0)) =>
        simplifyAll(e)
    case BinOp("*", e, Number(1)) =>
        simplifyAll(e)
    // 捕獲所有的 case 出現在更具體的簡化規則之後
    case UnOp(op, e) =>
        UnOp(op, simplifyAll(e))
    case BinOp(op, l, r) =>
        BinOp(op, simplifyAll(l), simplifyAll(r))
    case _ => expr
}

15.5 密封類

密封類除了在同一個檔案中定義的子類之外,不能新增新的子類。

sealed abstract class Expr

case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class Unop(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr

若定義了一個漏掉了某些可能的case的模式匹配,則編譯器會發出警告:

def describe(e: Expr): String = e match {
    case Number(_) => "a number"
    case Var(_) => "a variable"
}
// use @unchecked to suppress warning
def describe(e: Expr): String = (e: @unchecked) match {
    case Number(_) => "a number"
    case Var(_) => "a variable"
}

Option型別

Option`可以有兩種形式:`Some(x)`和`None
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
capitals get "France"
capitals get "North Pole"
def show(x: Option[String]) = x match {
    case Some(s) => s
    case None => "?"
}

show(capitals get "France")
show(capitals get "Japan")
show(capitals get "North Pole")

15.7 到處都是模式

變數定義中的模式

例如,可以將一個元組中的每個元素賦值給不同變數。

val myTuple = (123, "abc")

val (number, string) = myTuple

處理樣例類時非常有用。如果你知道要處理的樣例類是什麼,就可以用 一個模式來析構它。

val exp = new BinOp("*", Number(5), Number(1))
val BinOp(op, l, r) = exp

作為偏函式的case序列

{}包起來的一系列case語句可以用在任何允許出現函式字面量的地方。本質上講,case序列就是一個函式字面量。case序列可以有多個入口,多個引數列表。

val withDefault: Option[Int] => Int = {
    case Some(x) => x
    case None => 0
}

withDefault(Some(10))
withDefault(None)

通過case序列得到的是一個偏函式(partial function)。若將其應用到它不支援的值上,會產生一個執行時異常。

val second: List[Int] => Int = {
    case x :: y :: _ => y
}

second(List(5, 6, 7))
second(List())

若想檢查某個偏函式是否對某個入參有定義,必須首先告訴編譯器你要處理的是偏函式。

val second: PartialFunction[List[Int], Int] = {
    case x :: y :: _ => y
}

second.isDefinedAt(List(5, 6, 7))
second.isDefinedAt(List())
new PartialFunction[List[Int], Int] {
    def apply(xs: List[Int]) = xs match {
        case x :: y :: _ => y
    }
    
    def isDefinedAt(xs: List[Int]) = xs match {
        case x :: y :: _ => true
        case _ => false
    }
}

for表示式中的模式

for表示式可以從對映中接收key-value pairs,使其與模式匹配。例如:

for ((country, city) <- capitals)
    println("The capital of " + country + " is " + city)
The capital of France is Paris
The capital of Japan is Tokyo

可能存在某個模式不能匹配某個生成的值的情況。

val results = List(Some("apple"), None, Some("orange"))
for (Some(fruit) <- results) println(fruit)
apple
orange

可見生成的值當中那些不能匹配給定模式的值會被直接丟棄