
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 中並沒有 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 不太一樣的地方,第一個是變數的名稱現在是放在 其中另一個值得注意的地方,是上述的程式碼中並沒有 但如果我們今天需要讓數個不同的值都對應到相同的程式碼區塊的話,那可以用 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 敘述裡並沒有 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 程式碼的譯,我們卻會得到 會有這樣的結果,是因為在 Scala 中,case 右側的東西可以不只是「常數」,也可以是變數名稱,當我們這樣寫的時候,會把對應到的結果綁定到相對應的變數上,換句話說,上述的程式碼中第二行的 上面的程式碼實際上可以被簡化為下面這樣: val selection = 2 selection match { case selected => println("case 1: you selected " + 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 中如果我們要把某個變數注入到字串中,我們可以用 + 號將字串與變數連接起來,又或者可以用 不過在 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 此外,這個動作除了可以將變數注入字串外,也可以將運算式注入字串,而且同時也會經過型別檢查的確認,例如 關於 String Interpolation 的詳細介紹,可以參照 Scala 官方教學文件。 回到 Pattern Matching 本身的程式碼,我們可以發現有一些和 Java 的 switch / case 不一樣的地方。首先,我們的 scoreToLevel 函式的反回值的型別是字串,而我們的函式裡並沒有 return 關鍵字,而根據我們之前的介紹,在這個情況下函式實際上的返回值會是函式主體的最後後一個敘述。 而在這個函式裡,我們的最後一個敘述是 另一個要注意的地方,是在 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" } } 那麼 也可以檢查型別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 區塊,而這裡值得一提的是,我們會發現 當然,我們也可以結合型別檢查和 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 可以做的還不只這些,他另一個相當方便的地方是能夠直接針對特定類別的值進行匹配與擷取,透過這樣的方式,我們可以更清晰地表達我們的條件,至於這要如何應用,就留待下回揭曉。 |