當你在嘗試一門新的語言時,可能不會過于關注程序出錯的問題,但當真的去創造可用的代碼時,就不能再忽視代碼中的可能產生的錯誤和異常了。鑒于各種各樣的原因,人們往往低估了語言對錯誤處理支持程度的重要性。
事實會表明,Scala 能夠很優雅的處理此類問題,這一部分,我會介紹 Scala 基于 Try 的錯誤處理機制,以及這背后的原因。我將使用一個在 _Scala 2.10_ 新引入的特性,該特性向 _2.9.3_ 兼容,因此,請確保你的 Scala 版本不低于 _2.9.3_。
### 異常的拋出和捕獲
在介紹 Scala 錯誤處理的慣用法之前,我們先看看其他語言(如,Java,Ruby)的錯誤處理機制。和這些語言類似,Scala 也允許你拋出異常:
~~~
case class Customer(age: Int)
class Cigarettes
case class UnderAgeException(message: String) extends Exception(message)
def buyCigarettes(customer: Customer): Cigarettes =
if (customer.age < 16)
throw UnderAgeException(s"Customer must be older than 16 but was ${customer.age}")
else new Cigarettes
~~~
被拋出的異常能夠以類似 Java 中的方式被捕獲,雖然是使用偏函數來指定要處理的異常類型。此外,Scala 的 `try/catch` 是表達式(返回一個值),因此下面的代碼會返回異常的消息:
~~~
val youngCustomer = Customer(15)
try {
buyCigarettes(youngCustomer)
"Yo, here are your cancer sticks! Happy smokin'!"
} catch {
case UnderAgeException(msg) => msg
}
~~~
### 函數式的錯誤處理
現在,如果代碼中到處是上面的異常處理代碼,那它很快就會變得丑陋無比,和函數式程序設計非常不搭。對于高并發應用來說,這也是一個很差勁的解決方式,比如,假設需要處理在其他線程執行的 actor 所引發的異常,顯然你不能用捕獲異常這種處理方式,你可能會想到其他解決方案,例如去接收一個表示錯誤情況的消息。
一般來說,在 Scala 中,好的做法是通過從函數里返回一個合適的值來通知人們程序出錯了。別擔心,我們不會回到 C 中那種需要使用按約定進行檢查的錯誤編碼的錯誤處理。相反,Scala 使用一個特定的類型來表示可能會導致異常的計算,這個類型就是 Try。
#### Try 的語義
解釋 Try 最好的方式是將它與上一章所講的 Option 作對比。
`Option[A]` 是一個可能有值也可能沒值的容器,`Try[A]` 則表示一種計算:這種計算在成功的情況下,返回類型為 `A` 的值,在出錯的情況下,返回 `Throwable` 。這種可以容納錯誤的容器可以很輕易的在并發執行的程序之間傳遞。
Try 有兩個子類型:
1. `Success[A]`:代表成功的計算。
1. 封裝了 `Throwable` 的 `Failure[A]`:代表出了錯的計算。
如果知道一個計算可能導致錯誤,我們可以簡單的使用 `Try[A]` 作為函數的返回類型。這使得出錯的可能性變得很明確,而且強制客戶端以某種方式處理出錯的可能。
假設,需要實現一個簡單的網頁爬取器:用戶能夠輸入想爬取的網頁 URL,程序就需要去分析 URL 輸入,并從中創建一個 `java.net.URL` :
~~~
import scala.util.Try
import java.net.URL
def parseURL(url: String): Try[URL] = Try(new URL(url))
~~~
正如你所看到的,函數返回類型為 `Try[URL]`:如果給定的 url 語法正確,這將是 `Success[URL]`,否則, `URL` 構造器會引發 `MalformedURLException` ,從而返回值變成 `Failure[URL]` 類型。
上例中,我們還用了 Try 伴生對象里的 `apply` 工廠方法,這個方法接受一個類型為 `A` 的 _傳名參數_,這意味著, `new URL(url)` 是在 `Try` 的 `apply` 方法里執行的。
`apply` 方法不會捕獲任何非致命的異常,僅僅返回一個包含相關異常的 Failure 實例。
因此, `parseURL("http://danielwestheide.com")` 會返回一個 `Success[URL]` ,包含了解析后的網址,而 `parseULR("garbage")` 將返回一個含有 `MalformedURLException` 的 `Failure[URL]`。
#### 使用 Try
使用 Try 與使用 Option 非常相似,在這里你看不到太多新的東西。
你可以調用 `isSuccess` 方法來檢查一個 Try 是否成功,然后通過 `get` 方法獲取它的值,但是,這種方式的使用并不多見,因為你可以用 `getOrElse` 方法給 Try 提供一個默認值:
~~~
val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com")
~~~
如果用戶提供的 URL 格式不正確,我們就使用 DuckDuckGo 的 URL 作為備用。
#### 鏈式操作
Try 最重要的特征是,它也支持高階函數,就像 Option 一樣。在下面的示例中,你將看到,在 Try 上也進行鏈式操作,捕獲可能發生的異常,而且代碼可讀性不錯。
#### Mapping 和 Flat Mapping
將一個是 `Success[A]` 的 `Try[A]` 映射到 `Try[B]` 會得到 `Success[B]` 。如果它是 `Failure[A]` ,就會得到 `Failure[B]` ,而且包含的異常和 `Failure[A]` 一樣。
~~~
parseURL("http://danielwestheide.com").map(_.getProtocol)
// results in Success("http")
parseURL("garbage").map(_.getProtocol)
// results in Failure(java.net.MalformedURLException: no protocol: garbage)
~~~
如果鏈接多個 `map` 操作,會產生嵌套的 Try 結構,這并不是我們想要的。考慮下面這個返回輸入流的方法:
~~~
import java.io.InputStream
def inputStreamForURL(url: String): Try[Try[Try[InputStream]]] ` parseURL(url).map { u `>
Try(u.openConnection()).map(conn => Try(conn.getInputStream))
}
~~~
由于每個傳遞給 `map` 的匿名函數都返回 Try,因此返回類型就變成了 `Try[Try[Try[InputStream]]]` 。
這時候, `flatMap` 就派上用場了。`Try[A]` 上的 `flatMap` 方法接受一個映射函數,這個函數類型是 `(A) => Try[B]`。如果我們的 `Try[A]` 已經是 `Failure[A]` 了,那么里面的異常就直接被封裝成 `Failure[B]` 返回,否則, `flatMap` 將 `Success[A]` 里面的值解包出來,并通過映射函數將其映射到 `Try[B]` 。
這意味著,我們可以通過鏈接任意個 `flatMap` 調用來創建一條操作管道,將值封裝在 Success 里一層層的傳遞。
現在讓我們用 `flatMap` 來重寫先前的例子:
~~~
def inputStreamForURL(url: String): Try[InputStream] =
parseURL(url).flatMap { u =>
Try(u.openConnection()).flatMap(conn => Try(conn.getInputStream))
}
~~~
這樣,我們就得到了一個 `Try[InputStream]`,它可以是一個 Failure,包含了在 `flatMap` 過程中可能出現的異常;也可以是一個 Success,包含了最后的結果。
#### 過濾器和 foreach
當然,你也可以對 Try 進行過濾,或者調用 `foreach` ,既然已經學過 Option,對于這兩個方法也不會陌生。
當一個 Try 已經是 `Failure` 了,或者傳遞給它的謂詞函數返回假值,`filter` 就返回 `Failure`(如果是謂詞函數返回假值,那 `Failure` 里包含的異常是 `NoSuchException` ),否則的話, `filter` 就返回原本的那個 `Success` ,什么都不會變:
~~~
def parseHttpURL(url: String) ` parseURL(url).filter(_.getProtocol `= "http")
parseHttpURL("http://apache.openmirror.de") // results in a Success[URL]
parseHttpURL("ftp://mirror.netcologne.de/apache.org") // results in a Failure[URL]
~~~
當一個 Try 是 `Success` 時, `foreach` 允許你在被包含的元素上執行副作用,這種情況下,傳遞給 `foreach` 的函數只會執行一次,畢竟 Try 里面只有一個元素:
~~~
parseHttpURL("http://danielwestheide.com").foreach(println)
~~~
> 當 Try 是 Failure 時, `foreach` 不會執行,返回 `Unit` 類型。
### for 語句中的 Try
既然 Try 支持 `flatMap` 、 `map` 、 `filter` ,能夠使用 for 語句也是理所當然的事情,而且這種情況下的代碼更可讀。為了證明這一點,我們來實現一個返回給定 URL 的網頁內容的函數:
~~~
import scala.io.Source
def getURLContent(url: String): Try[Iterator[String]] =
for {
url <- parseURL(url)
connection <- Try(url.openConnection())
is <- Try(connection.getInputStream)
source = Source.fromInputStream(is)
} yield source.getLines()
~~~
這個方法中,有三個可能會出錯的地方,但都被 Try 給涵蓋了。第一個是我們已經實現的 `parseURL` 方法,只有當它是一個 `Success[URL]` 時,我們才會嘗試打開連接,從中創建一個新的 `InputStream` 。如果這兩步都成功了,我們就 `yield` 出網頁內容,得到的結果是 `Try[Iterator[String]]` 。
當然,你可以使用 `Source#fromURL` 簡化這個代碼,并且,這個代碼最后沒有關閉輸入流,這都是為了保持例子的簡單性,專注于要講述的主題。
> 在這個例子中,`Source#fromURL`可以這樣用:
~~~
import scala.io.Source
def getURLContent(url: String): Try[Iterator[String]] =
for {
url <- parseURL(url)
source = Source.fromURL(url)
} yield source.getLines()
~~~
> 用 `is.close()` 可以關閉輸入流。
#### 模式匹配
代碼往往需要知道一個 Try 實例是 Success 還是 Failure,這時候,你應該想到模式匹配,也幸好, `Success` 和 `Failure` 都是樣例類。
接著上面的例子,如果網頁內容能順利提取到,我們就展示它,否則,打印一個錯誤信息:
~~~
import scala.util.Success
import scala.util.Failure
getURLContent("http://danielwestheide.com/foobar") match {
case Success(lines) => lines.foreach(println)
case Failure(ex) => println(s"Problem rendering URL content: ${ex.getMessage}")
}
~~~
#### 從故障中恢復
如果想在失敗的情況下執行某種動作,沒必要去使用 `getOrElse`,一個更好的選擇是 `recover` ,它接受一個偏函數,并返回另一個 Try。如果 `recover` 是在 Success 實例上調用的,那么就直接返回這個實例,否則就調用偏函數。如果偏函數為給定的 `Failure` 定義了處理動作,`recover` 會返回 `Success` ,里面包含偏函數運行得出的結果。
下面是應用了 `recover` 的代碼:
~~~
import java.net.MalformedURLException
import java.io.FileNotFoundException
val content = getURLContent("garbage") recover {
case e: FileNotFoundException => Iterator("Requested page does not exist")
case e: MalformedURLException => Iterator("Please make sure to enter a valid URL")
case _ => Iterator("An unexpected error has occurred. We are so sorry!")
}
~~~
現在,我們可以在返回值 `content` 上安全的使用 `get` 方法了,因為它一定是一個 Success。調用 `content.get.foreach(println)` 會打印 _Please make sure to enter a valid URL_。
### 總結
Scala 的錯誤處理和其他范式的編程語言有很大的不同。Try 類型可以讓你將可能會出錯的計算封裝在一個容器里,并優雅的去處理計算得到的值。并且可以像操作集合和 Option 那樣統一的去操作 Try。
Try 還有其他很多重要的方法,鑒于篇幅限制,這一章并沒有全部列出,比如 `orElse` 方法,`transform` 和 `recoverWith` 也都值得去看。
下一章,我們會探討 Either,另外一種可以代表計算的類型,但它的可使用范圍要比 Try 大的多。