Scala Tutorial(10)不只可以列舉值的 Pattern Matching by brianhsu | CodeData
top

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

分享:

Scala Tutorial(9)Option[T] 簡介 << 前情

前兩篇我們介紹了 Tuple 和 Option[T] 這兩個 Scala 中常用的資料結構,有了這些基本的認知之後,我們可以就開始仔細看看 Scala 除了 if / else 之外,還提供了什麼樣的流程控制功能。

長得有點不一樣的 match

在 Java 裡面,如果我們要針對不同的條件執行不同的程式碼時,除了使用 if 區塊外,也常常可以使用 switch / case 的語法,透過檢視某個變數或運算示是否是特定的值,來決定要進入哪一段程式碼。

例如下面的程式碼中,會依照 selection 的值選擇要印出哪一段訊息。

int selection = 2;

switch(selection) {
  case 1:
    System.out.println("Selected 1");
    break;
  case 2:
    System.out.println("Selected 2");
    break;
  case 3:
    System.out.println("Selected 3");
    break;
  default:
    System.out.println("Other");
    break;
}

不過另一方面,在 Scala 中並沒有 switch 這個關鍵字,若要達成相同的效果,則要使用 match 這個關鍵字,這種語法在 Scala 裡面有一個名字--Pattern Matching:

val selection = 2
selection match {
  case 1 => println("Selected 1")
  case 2 => println("Selected 2")
  case 3 => println("Selected 3")
  case _ => println("Other")
}

在這邊我們可以看到幾個和 Java 中的 switch 不太一樣的地方,第一個是變數的名稱現在是放在 match 關鍵字之前,第二個是我們的 case 敘述後是使用 => 來表示分格,第三個是 default 被改成 case _ 了。

其中另一個值得注意的地方,是上述的程式碼中並沒有 break 敘述,這是因為和 Java 不同,Scala 的 Pattern Matching 並不會有 Fall-through 的情況,他只會執行相對應的程式碼區塊,不會執行另一個 case 裡的內容。

但如果我們今天需要讓數個不同的值都對應到相同的程式碼區塊的話,那可以用 | 符號把多個條件串連起來如下:

val selection = 1
selection match {
  case 1|2|3 => println("Selected 1 or 2 or 3")
  case 4 => println("Selected 4")
  case _ => println("Other")
}

此外,要注意的是 Pattern Matching 一定要有符合的對應的程式碼區塊,否則會丟出 MatchError 這個執行期的錯誤。像下面的程式碼,因為 match 敘述裡並沒有 case 10 這個執行路徑,也沒有代表預設的 case _,所以執行時會丟出 MatchError 錯誤。

val selection = 10

// 將會丟出 MatchError 錯誤
selection match {
  case 1 => println("Selected 1")
  case 2 => println("Selected 2")
}

他真的不是 switch / case

雖然就表面上來看,Pattern Matching 似乎只是換了另一個語法的 switch 敘述而已,但透過下面的這段程式碼,我們就可以很清楚地知道 Scala 中的 Pattern Matching 和 switch 在本質上是有很大的不同的。

val selection = 2
val one = 1

selection match {
  case one => println("case 1: you selected " + one)
}

在執行這段程式碼之前,不妨先猜猜看最後的執行結果會是什麼。如果是從 Java 的 switch 來想,可能會認為不會有任何輸出,但實際上執行這段 Scala 程式碼的譯,我們卻會得到 case 1: you selected 2 的訊息。

會有這樣的結果,是因為在 Scala 中,case 右側的東西可以不只是「常數」,也可以是變數名稱,當我們這樣寫的時候,會把對應到的結果綁定到相對應的變數上,換句話說,上述的程式碼中第二行的 one 變數和 case 關鍵字右方的 one 變數是完全沒有關係的。

上面的程式碼實際上可以被簡化為下面這樣:

val selection = 2

selection match {
  case selected => println("case 1: you selected " + selected)
}

如果我們試著把 selection 改成別的值,會發現這個 case 後的程式碼永遠都會成立,甚至當我們把 selection 改成字串時,同樣也會成立。這是因為我們並沒有特別為 selected 這條條件式設下什麼限制,所以他永遠都會成立。

使用 if 來限定條件

話說在 Java 當中,switch / case 是使用列舉「單一值」的方式來決定要執行哪一段程式碼,因此若要達成像是成績在 100 ~ 90 應到 A,在 89 ~ 80 對應到 B,79 ~ 60 對應到 C……這樣的條件的話,除非將每個分數都列舉出來的話,是無法達成的。

但另一方面,在 Scala 中的 Pattern Matching 比較像是條列式的 if / else 變型語法,因此我們除了可以使用列舉值之外,也可以在 case 條件後使用 if 來限制該 case 成立時的條件。

舉例而言,將十進位分數對應至分數等級的程式碼,在 Scala 中我們可以這麼寫:

def scoreToLevel(score: Int): String = {
  score match {
    case scoreA if scoreA >= 90 => 
      println("Mapping " + scoreA + " to A")
      "A"
    case scoreB if scoreB >= 80 => 
      println(s"Mapping $scoreB to B")
      "B"
    case scoreC if scoreC >= 70 => 
      println(s"Mapping $scoreC to C")
      "C"
    case scoreD =>
      println(s"Mapping $scoreD to D")
      "D"
  } 
}

println(scoreToLevel(40))
println(scoreToLevel(53))
println(scoreToLevel(83))
println(scoreToLevel(100))

執行之後的輸出如下:

Mapping 40 to D
D
Mapping 53 to D
D
Mapping 83 to B
B
Mapping 100 to A
A

在這段程式碼中我們引入了一樣之前沒介紹過,但值得稍微提一下的東西--格式化字串。在 Java 中如果我們要把某個變數注入到字串中,我們可以用 + 號將字串與變數連接起來,又或者可以用 String.format() 函式來格式化字串。

不過在 Scala 2.10 後引入了一個叫 String Interpolation 的功能,他讓我們可以在字串中直接加入變數,當我們在字串的雙引後前入 s 後,Scala 的編譯器就會將字串中出現的 $ 開頭的變數名稱代換成在 scope 內的變數:

scala> val myInt = 1234
myInt: Int = 1234

scala> s"Hello, this is your variable: $myInt"
res0: String = Hello, this is your variable: 1234

此外,這個動作除了可以將變數注入字串外,也可以將運算式注入字串,而且同時也會經過型別檢查的確認,例如 s"Today is ${(new java.util.Date).toSting()}" 這樣的字串會產生編譯錯誤,因為 java.util.Date 裡並沒有 toSting() (少了一個 r)這樣的方法。

關於 String Interpolation 的詳細介紹,可以參照 Scala 官方教學文件

回到 Pattern Matching 本身的程式碼,我們可以發現有一些和 Java 的 switch / case 不一樣的地方。首先,我們的 scoreToLevel 函式的反回值的型別是字串,而我們的函式裡並沒有 return 關鍵字,而根據我們之前的介紹,在這個情況下函式實際上的返回值會是函式主體的最後後一個敘述。

而在這個函式裡,我們的最後一個敘述是 match 關鍵字,換句話說,Pattern Matching 是有返回值的,而且他的返回值正好就是相對應的 case 區塊的最後一個敘述。而透過這樣的特性,我們可以將 Pattern Matching 的結果賦值給另一個變數或是當做函式的返回值!這一點和 Java 中的 switch / case 相當不一樣。

另一個要注意的地方,是在 case 後的 if 條件式,是可以使用到 case 條件的變數的,雖然目前看來這沒有什麼太大的用處,不過等到我們進入到進階的 Pattern Matching 應用時,就會發現這樣的特性相當方便。

最後的最後,要注意的是 Pattern Matching 是一路從第一個 case 檢查,而且檢查到符合條件的 case 後就不再繼續進行下去,因此如果我們把上述的程式碼改成下面這樣:

def scoreToLevel(score: Int): String = {
  score match {
    case scoreB if scoreB >= 80 => "B"
    case scoreA if scoreA >= 90 => "A"
    case scoreC if scoreC >= 70 => "C"
    case scoreD => "D"
  } 
}

那麼 scoreToLevel(100) 的返回值將會是 “B",因為第一個 case 已經符合了,所以就會直接執行第一個 case 的程式碼並返回最後一個運算式的值,而不會再去檢查第二個條件式。

也可以檢查型別

Pattern Matching 除了可以使用列舉值或是像上一小節一樣使用 if 來判斷要執行哪個 case 之外,我們也可以借助 Pattern Matching 來達成「依照不同型別執行不同的程式碼」。

在 Java 中,若我們要達成這樣的功能,會使用 instanceOf 這個運算子並且搭配強制轉型語法來使用,不過若使用 Scala 的 Pattern Matching 的話,就可以很容易的將兩件事情結合在一起:

import java.util.Date

def matchDifferntType(x: Any) = {
  x match {
    case myInt: Int       => println("myInt / 2 = " + myInt / 2)
    case myString: String => println("myString.length = " + myString.length)
    case myDate: Date     => println("myDate.getTime = " + myDate.getTime)
  }
}

matchDifferntType(22)            // 印出 myInt / 2 = 11
matchDifferntType("HelloWorld")  // 印出 myString.length = 10
matchDifferntType(new Date)      // 印出 myDate.getTime = 1407988803137
matchDifferntType('c')           // 產生執行期錯誤,因為找不到可以執行的分支

在這個例子中,當在 case 條件的變數名稱後加上了型別限制,透過這樣的方式,只有當傳入的 x 符合指定的型別的時候,才會進入相對應的 case 區塊,而這裡值得一提的是,我們會發現 myIntmyString 以及 myDate 都被直接被轉型成相對應的型別,而可以呼叫該型別特有的方法。

當然,我們也可以結合型別檢查和 if 判斷式,來限縮某個 case 可以對應到的範圍,例如我們可以限制 myInt 只接授偶數:

def isOdd(x: Int) = x % 2 == 0
def matchDifferntType(x: Any) = {
  x match {
    case myInt: Int if isOdd(myInt)  => println("myInt / 2 = " + myInt / 2)
    case myString: String => println("myString.length = " + myString.length)
    case myDate: Date     => println("myDate.getTime = " + myDate.getTime)
    case _ => println("I don't know what you're talking about")
  }
}

matchDifferntType(20)  // 印出 myInt / 2 = 10
matchDifferntType(3)   // 印出 I don't know what you're talking about

小結

這次我們介紹了 Pattern Matching 這個和 Java 的 switch / case 有點類似,但又不太相同的流程控制結構,並且看到 Pattern Matching 如何讓我們不只能夠使用列舉的方式來表達條件,也能針對資料的型別進行匹配和轉型,甚至可以在 case 後加入 if 條件式來限縮匹配的範圍。

不過事實上 Pattern Matching 可以做的還不只這些,他另一個相當方便的地方是能夠直接針對特定類別的值進行匹配與擷取,透過這樣的方式,我們可以更清晰地表達我們的條件,至於這要如何應用,就留待下回揭曉。

後續 >> Scala Tutorial(11)Pattern Matching 與 Case Class

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

相關文章

留言

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

熱門論壇文章

熱門技術文章