Scala Tutorial(11)Pattern Matching 與 Case Class by brianhsu | CodeData
top

Scala Tutorial(11)Pattern Matching 與 Case Class

分享:

Scala Tutorial(10)不只可以列舉值的 Pattern Matching << 前情

使用 Pattern Matching 來比較 Option[T]

在上一篇中,我們看到了在 Scala 中 match 這個關鍵字的基本用法,也看到相較於 Java 中的 switch 來說,Pattern Matching 除了可以用列舉值的方式來決定程式的分歧執行路徑外,也可以外加額外的條件,甚至比對傳入值的型別等等。

不過 Pattern Matching 並不只有這些功能而已,他甚至能夠在「物件」的層級進行比對。

還記得嗎?我們曾經在前幾篇介紹過在 Scala 中,我們經常用用 Option[T] 這個型別來表示一個可能為空的函式回傳值,而這個型別有兩種子類別,當有值的時候我們會回傳 Some(x),並將 x 代入實際的值,而當沒有值的時候,我們會回傳 None 這個物件。

而在之前的程式碼中,我們是使用傳統的 if / else 區塊來判斷目前的 Option[T] 物件是否有值,並決定要如何處理:

def printOptionContent(box: Option[Int]) {
  if (box.isDefined) {
    println("盒子裡放的是:" + box.get)
  } else {
    println("盒子裡是空的")
  }
}

printOptionContent(Some(12))  // 盒子裡放的是:12
printOptionContent(None)      // 盒子裡是空的

除了使用這樣的方式外,我們也可以用 Pattern Matching 來做處理,上述的程式可以被改寫為:

def printOptionContent(box: Option[Int]) = {
  box match {
    case Some(value) => println("盒子裡放的是:" + value)
    case None => println("盒子裡是空的")
  }
}

printOptionContent(Some(12))  // 盒子裡放的是:12
printOptionContent(None)      // 盒子裡是空的

在這裡要注意的是,我們在 Some(value) 右側的區塊中,這個 value 變數是直接被綁定到 Some 物件裡實際放置的值,所以我們並不需要再呼叫 .get 方法來取得值。

此外,由於在 Scala 當中 Pattern Matching 也是有回傳值的,他的回傳值就是每一個 => 區塊的最後的一個敘述,所以我們也可以把上面的 printOptionContent 函式,改成返回對應的字串,而不是直接印出來:

def optionContent(box: Option[Int]): String = {
  box match {
    case Some(value) => "盒子裡放的是:" + value
    case None => "盒子裡是空的"
  }
}

println(optionContent(Some(12)))  // 盒子裡放的是:12
println(optionContent(None))      // 盒子裡是空的

也可以匹配 Case Class

當然 Pattern Matching 除了匹配 Option[T] 之外,也可以用來匹配其他的物件,甚至可以比對我們自己訂義的 case class 類別,例如我們可以定義一個名為 Person 的類別,並且給他 name 與 age 兩個參數:

case class Person(name: String, age: Int)

這個時候,我們就可以使用 Pattern Matching 來表示我們想要處理的各種狀況:

def personCheck(person: Person) = {
  person match {
    case Person("Joe", 15) => "名為 Joe 且 15 歲的人"
    case Person("Joe", age) => "名為 Joe,但不是 15 歲而是 $age 歲的人"
    case Person(name, age) if age >= 20 => s"名為 $name 而且大於 20 歲的人"
    case _ => "其他人"
  }
}

自己定義如何匹配

讀者可能會好奇這是怎麼做到的,又或者例如當看到下面的程式碼時,若我們回頭去翻 Scala 的標準函式庫的 API 文件,會發現 List 類別並不是 case class,那又為什麼他可以使用 Pattern Matching 這樣的功能來匹配呢?

val xs = List(12, 14, 16)

xs match {
  case List(x, y, z) => "x y z 分別是 $x, $y, $z"
  case _ => "這個 List 的長度不等於 3"
}

還記得我們之前講過當我們訂義 case class 的時候,其實是 Scala 的編譯器自動幫我們替這個類別加入一些函式和相對應的 Singleton 物件嗎?同樣的,這次同樣是因為 Scala 編譯器自動幫我們加入了 unapply 這個函式,所以我們可以在新定義的類別上使用 Pattern Matching。

既然他只是自動幫我們加了某個函式進去,我們當然也可以手動製作出這種效果。

首先我們試試看如果把 Person 物件的關建字拿掉,會發生什麼事:

class Person(val name: String, val age: Int)

def personCheck(person: Person) = {
  person match {
    case Person("Joe", 15) => "名為 Joe 且 15 歲的人"
    case Person("Joe", age) => "名為 Joe,但不是 15 歲而是 $age 歲的人"
    case Person(name, age) if age >= 20 => s"名為 $name 而且大於 20 歲的人"
    case _ => "其他人"
  }
}

println(personCheck(new Person("Joe", 15)))   // "名為 Joe 且 15 歲的人"

這個時候,會發生編譯錯誤,而且訊息會有點怪,會告訴你 not found: value Person 這種奇怪的訊息,而且出錯的地方是 case 敘述的 Person 的開頭。

這是因為當 Scala 看到我們寫類似 case Person("Joe", 15) 這樣的條件時,實際上是會把要進行 Pattern Matching 的值傳給 Person 這個 Singleton 物件的 unapply 方法,而這個 unapply 方法會回傳一個 Option 物件,而這個 Option 物件內放的值若符合 case 的條件時,那麼 => 右側的敘述就會被執行。

從最簡單的一個例子來看,若我們自己訂義了一個叫做 class IntegerBox(val value: Int) 這樣的簡單類別,若要讓他支援 Pattern Matching 的話,那麼只要加入一個 IntergerBox 的 Singleton 物件,並且實作一個接受 IntegerBox 物件,並返回 Option[Int] 類別的 unapply 方法就可以了:

class IntegerBox(val value: Int)

object IntegerBox {
  def unapply(box: IntegerBox): Option[Int] = Some(box.value)
}

def checkBox(box: IntegerBox) = {
  box match {
    case IntegerBox(1) => "盒子裡放的是 1"
    case IntegerBox(x) if x >= 10 => "盒子裡放的是大於等於 10 的數字"
    case _ => "盒子裡放的是其他東西"
  }
}

println(checkBox(new IntegerBox(1)))    // "盒子裡放的是 1"
println(checkBox(new IntegerBox(5)))    // "盒子裡放的是其他東西"
println(checkBox(new IntegerBox(20)))   // "盒子裡放的是大於等於 10 的數字"

另外這邊要注意的是,unapply 只有在返回 Some 的時候才會匹配成功,如果我們故意讓他在 box.value == 1 時返回 None 的話,那我們在 match 當中的第一條規則就不會成立:

class IntegerBox(val value: Int)

object IntegerBox {
  def unapply(box: IntegerBox): Option[Int] = if (box.value == 1) None else Some(box.value)
}

def checkBox(box: IntegerBox) = {
  box match {
    case IntegerBox(1) => "盒子裡放的是 1"
    case _ => "盒子裡放的是其他東西"
  }
}

println(checkBox(new IntegerBox(1))) // 盒子裡面的是其他東西

那如果像上面的 Person 一樣,我們要匹配的物件,有不只一個成員變數的話要怎麼辦呢?很簡單,我們在 unapply 函式時,反回的是相對應的 Tuple 的 Option 物件就好了,以 Person 為例,我們要匹配的部份依序是 name 字串和 age 整數,那就讓 unapply 返回 Option[(String, Int)] 這樣就行了。

class Person(val name: String, val age: Int)

object Person {
  def unapply(person: Person): Option[(String, Int)] = Some((person.name, person.age))
}

def personCheck(person: Person) = {
  person match {
    case Person("Joe", 15) => "名為 Joe 且 15 歲的人"
    case Person("Joe", age) => "名為 Joe,但不是 15 歲而是 $age 歲的人"
    case Person(name, age) if age >= 20 => s"名為 $name 而且大於 20 歲的人"
    case _ => "其他人"
  }
}

println(personCheck(new Person("Joe", 15)))   // "名為 Joe 且 15 歲的人"

Pattern Matching 中的運算元

此外,這裡另一個要注意的是我們要匹配的物件的類別,是可以和訂義 unapply 方法的 Singleton 物件不同名的,例如我們可以很故意的把上一小節的 object Person 改成 object Ghost,然後在 Pattern Matching 時把 case 右側的 Person 改成 Ghost,這樣也是可以正常運作的。

class Person(val name: String, val age: Int)

object Ghost {
  def unapply(person: Person): Option[(String, Int)] = Some((person.name, person.age))
}

def personCheck(person: Person) = {
  person match {
    case Ghost("Joe", 15) => "名為 Joe 且 15 歲的人"
    case Ghost("Joe", age) => "名為 Joe,但不是 15 歲而是 $age 歲的人"
    case Ghost(name, age) if age >= 20 => s"名為 $name 而且大於 20 歲的人"
    case _ => "其他人"
  }
}

println(personCheck(new Person("Joe", 15)))

另外,由於 Ghost 有兩個參數,所以也可以在 case 敘述的右邊使用運算元的方式來使用:

def personCheck(person: Person) = {
  person match {
    case "Joe" Ghost 15 => "名為 Joe 且 15 歲的人"
    case "Joe" Ghost age => "名為 Joe,但不是 15 歲而是 $age 歲的人"
    case name Ghost age if age >= 20 => s"名為 $name 而且大於 20 歲的人"
    case _ => "其他人"
  }
}

println(personCheck(new Person("Joe", 15)))

當然,這樣看起來怪怪的,但如果我們把 object Ghost 改成 object @@ 的話,他看起來似乎就像是一般的 infix 運算元了:

object @@ {
  def unapply(person: Person): Option[(String, Int)] = Some((person.name, person.age))
}

def personCheck(person: Person) = {
  person match {
    case "Joe" @@ 15 => "名為 Joe 且 15 歲的人"
    case "Joe" @@ age => "名為 Joe,但不是 15 歲而是 $age 歲的人"
    case name  @@ age if age >= 20 => s"名為 $name 而且大於 20 歲的人"
    case _ => "其他人"
  }
}

有了這樣的背景知識的話,我們就可以理解當我們在 Scala 的程式碼當中看到,針對 List 做 Pattern Matching 時常常會看到的形式:

def checkList(xs: List[Int]) = xs match {
  case head :: Nil => "只有一個元素的 List"
  case 1 :: _ => "開頭是 1,長度不拘的 List"
  case w :: x :: y :: 10 :: Nil => "總共有四個元素的 List,最後一個是 10"
  case _ => "其他的 List"
}

println(checkList(List(1,2,3)))
println(checkList(List(3,2,3,10)))
println(checkList(List(10)))

其實等同於下面的程式碼,而其中的運算元 :: 其實也只不過就是一個 Scala 函式庫內建的 case class 類別罷了。

def checkList(xs: List[Int]) = xs match {
  case ::(head, Nil) => "只有一個元素的 List"
  case ::(1,  _) => "開頭是 1,長度不拘的 List"
  case ::(w, ::(x, ::(y, ::(10, Nil)))) => "總共有四個元素的 List,最後一個是 10"
  case _ => "其他的 List"
}

println(checkList(List(1,2,3)))
println(checkList(List(3,2,3,10)))
println(checkList(List(10)))

小結

這次我們介紹了 Scala 當中 Pattern Matching 的進階用法,以及要如何自己訂義在 Pattern Matching 當中 case 右側的匹配條件,甚至是創造自己在做 Pattern Matching 時可以使用的運算元。

在 Scala 當中,Pattern Matching 用運得當的話,會是很強大的工具,可以讓程式的巢狀式 if / else 直接成為單一層的 Pattern Matching 列表,也可以直接在進行匹配的過程中就將符合條件的物件的成員綁定到變數上,讓程式看起來更清晰。

接下來,我們會介紹 Scala 當中的異於常人,初看詭異,但使用久了之後卻會漸漸發現原來他是有道理的 for 迴圈。至於他怎麼個詭異法,就留待下回分曉囉。

分享:
按讚!加入 CodeData Facebook 粉絲群

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

熱門論壇文章

熱門技術文章