[Scala] 2.10 新增的 Try 錯誤處理類別

前言

Scala 的設計理念中,有一個很有趣的看法,就是沒有最重要的編程典範,也沒有最萬能的工具,一切都是看適不適合而已。

所以在寫 Scala 的時候,會隱隱約約地感覺得出來,雖然物件導向可以解決很多的問題,但對 Scala 來講他並不是至高無上的準則;雖然 Functional Programming 很強,但對 Scala 的程式而言,他也並不是所有問題的唯一解答。

同樣的,雖然 Immutable 的資料結構很方便,可是有的時候,有些事情,用迴圈和 Mutable 的變數來做,反而會比較清楚。所以雖然 Scala 預設和偏好使用各種 Immutable 的方式來做事,但仍然給了你像是 var 變數和 Mutable 的資料結構這些東西。

所以 Scala 比較像一個完整的工具箱--裡面有各式各樣的工具,看當下的工作適合哪個工具,就拿哪個工具出來用。

沒錯,的確有的時候我們會拿老虎鉗來釘釘子,但如果需要釘大量的釘子,去搞個榔頭來敲會事半功倍一些。

所以 Scala 在實際上使用起來,他反而比較接近 Perl 的一句格言--There is more than one way to do it--一件事情可以有好幾種作法。

至於到底要用哪一種方法來做事,就看當下的情境和工具的順手程度。

也因為這樣,當我今早起來,在 Google Reader 上看到明明已經有了我們熟悉的 try-catch block,和用 Functional Programming 思維來處理 Exception 的 Catch 類別的 Scala 2.10,又加了另一個新的錯誤處理機制,也就不覺得奇怪了。

相反的,反而會想知道這個新出來的東西有什麼神奇的地方。:p

傳統的 try-catch 區塊

這裡以一個簡單的 Integer.parseInt() 來舉例,在 Java / Scala 裡這個函式可以把字串轉換成整數,然後當他發生錯誤時(例如你給他一個 "XXX" 字串的話,他當然無法轉成整數),就會丟出一個 Exception 出來。

假設我們今天希望進行的處理是「用 Integer.parseInt 把字串轉為整數,如果失敗的話,就給他一個 Error Code -1」,已經習慣 Java 的錯誤處理方式的人,剛轉到 Scala 時可能會這麼寫:

val rawString = "XXXX"
var result = -1

try {
    result = Integer.parseInt(rawString)
} catch {
    case e: Exception => // 吃掉 Exception,因為 result 本來就是 -1
}

println("result = " + result)

上面這段程式並沒有什麼問題,確實可以達到我們想要的結果,如果 rawString 裡的東西可以轉成整數,result 就會被設為那個值,如果不行,就會維持 Error Code 是 -1。

有傳回值的 try-catch 區塊

然後寫了一段時間 Scala 的人,看到上面的那段程式,就會覺得混身不對勁,想要把 var 給消滅掉,盡量讓一個變數只會被指定一次,然後就不再更改。

這個時候,Scala 的 try-catch 另一個特性就浮現出來了--Scala 裡的 try-catch 區塊是一個 expression,也就是說他是可以有傳回值的!

所以我們原本的程式可以改寫成像下面這樣:

val rawString = "100"
val result = try {
    Integer.parseInt(rawString)
} catch {
    case e: Exception => -1
}

println(result)

這個時候,我們就可以把 var 消滅掉,result 這個變數只會被指定一次,之後就不會再更改了。

Functional 的 Exception catch

Note

下面的這幾個錯誤處理方式,雖然看起來很像是 Scala 語言內建的語法,但實際上卻都只是函式庫而已。這是 Scala 滿有趣的一點,他的某些彈性讓你可以設計出像是內建語法的函式庫。XD

當然上面的方法已經不錯了,但就像上面講到的,Scala 習慣給你各種工具,有的人會覺得上面的程式碼還是不夠 Functional,所以 Scala 也提供了另一個例外處理方式--在 scala.util.control.Exception 這個 Singleton 物件裡。

這個物件提供了一些比較 Functional 的錯誤處理方式,可以讓上面的程式碼更 Functional 一點。

舉例來講,在 Functional 的方法中,比起出錯時給 Error Code(畢竟有的時候你很難找到一個合理的預設值代表錯誤),我們更喜歡用一種能表示「這是可能沒有合理值」的東西,在 Scala 裡這個東西就是 Option 類別。

你可以把 Option 類別看成一個箱子,箱子裡要嘛有東西(在 Scala 裡用 Some 這個子類別表示),要嘛他裡面是空的(在 Scala 裡用 None 表示)。

在這種情況下,上面的程式裡的 result,我們會比較希望他是一個 Option[Int],而不是單純的 Int。

這個時候,就可以用 scala.util.control.Exception.catching() 這個函式來達成:

import scala.util.control.Exception._

val rawString = "100"
val result: Option[Int] = catching(classOf[Exception]).opt(Integer.parseInt(rawString))

println(result)     // 印出 Some(100)
import scala.util.control.Exception._

val rawString = "XXXX"
val result: Option[Int] = catching(classOf[Exception]).opt(Integer.parseInt(rawString))

println(result)     // 印出 None

像上面一樣,透過 catching() 這個函式配合上 Option,我們可以讓「到底有沒有正確的回傳值」這件事變得更顯眼,而由於 Option[T] 有許多特異功能,所以在之後的處理上會方便很多。

另外你也會看到,在這次的程式碼比起原來的又簡潔了許多--整個變成只有一行了。

基本上這句的意思,就是「先執行 opt 裡的程式碼,如果沒有 Exception 就回傳 Some(opt 裡的程式碼的程式結果),如果有 Exception,就傳回 None」。

不過老實講,我自己不太習慣這樣的表達方式,常常看到這句寫法之後,會頭昏腦脹很久之後,才看出整個錯誤處理的流程到底是啥。

新的 Try 類別

相較之下,這次 Scala 2.10 裡新引入的 Try 類別感覺上就比較吸引我了,而且使用起來比較直覺一點,一樣直接來看程式碼吧,這次的程式碼變這樣了:

import scala.util.Try

val rawString = "xxx"

//
// 在 Try 裡包可能會出錯的程式碼,Try 和 Option 一樣,有不同的子類別代表不同的
// 狀態,如果返回的是 Success 這個子類別,代表一切正常,沒有 Exception 發生。
//
// 如果返回的是 Failure,代表有 Exception 發生,這個時候看你要直接給預設值還是
// 做錯誤處理啥的都行。
//
val result: Try[Int] = Try(Integer.parseInt(rawString))

println(result.get)           // 如果成功就是原來的值,如果有 Exception 發生就丟 Exception
println(result.getOrElse(-1)) // 如果成功就是原來的值,如果有 Exception 發生就是 -1

// 如果 result 本來是 Failure 的狀態,就用下面的區塊來做錯誤處理,並反回 -1
val result2 = result.recover {
    case e: Exception => -1
}

println(result2.get)

我自己是覺得這次的 Try 有了上面的 catching 函式的優點,但在使用上語義的表達要更清晰了一點--我試著做一件事,他可能會成功(Success)或失敗(Failure),然後你再看成功和失敗時分別要處理什麼事情就可以了。:p

回響