一.概述 當你在嘗試一門新的語言時,可能不會過於關註程式出錯的問題, 但當真的去創造可用的代碼時,就不能再忽視代碼中的可能產生的錯誤和異常了。 鑒於各種各樣的原因,人們往往低估了語言對錯誤處理支持程度的重要性。 事實會表明,Scala 能夠很優雅的處理此類問題, 這一部分,我會介紹 Scala 基於 ...
一.概述
當你在嘗試一門新的語言時,可能不會過於關註程式出錯的問題, 但當真的去創造可用的代碼時,就不能再忽視代碼中的可能產生的錯誤和異常了。 鑒於各種各樣的原因,人們往往低估了語言對錯誤處理支持程度的重要性。
事實會表明,Scala 能夠很優雅的處理此類問題, 這一部分,我會介紹 Scala 基於 Try 的錯誤處理機制,以及這背後的原因。 我將使用一個在 Scala 2.10 新引入的特性,該特性向 2.9.3 相容, 因此,請確保你的 Scala 版本不低於 2.9.3。
二.異常拋出與捕獲
2.1 其他語言的錯誤處理機制
在介紹 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
}
2.2 函數式的錯誤處理
現在,如果代碼中到處是上面的異常處理代碼,那它很快就會變得醜陋無比,和函數式程式設計非常不搭。 對於高併發應用來說,這也是一個很差勁的解決方式,比如, 假設需要處理在其他線程執行的 actor 所引發的異常,顯然你不能用捕獲異常這種處理方式, 你可能會想到其他解決方案,例如去接收一個表示錯誤情況的消息。
一般來說,在 Scala 中,好的做法是通過從函數里返回一個合適的值來通知人們程式出錯了。 別擔心,我們不會回到 C 中那種需要使用按約定進行檢查的錯誤編碼的錯誤處理。 相反,Scala 使用一個特定的類型來表示可能會導致異常的計算,這個類型就是 Try。
Try 的語義
解釋 Try 最好的方式是將它與 Option 作對比。
Option[A] 是一個可能有值也可能沒值的容器, Try[A] 則表示一種計算: 這種計算在成功的情況下,返回類型為 A 的值,在出錯的情況下,返回 Throwable 。 這種可以容納錯誤的容器可以很輕易的在併發執行的程式之間傳遞。
Try 有兩個子類型:
- Success[A]:代表成功的計算。
- 封裝了 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
3.1 初步使用 Try
使用 Try 與使用 Option 非常相似,在這裡你看不到太多新的東西。
你可以調用 isSuccess 方法來檢查一個 Try 是否成功,然後通過 get 方法獲取它的值, 但是,這種方式的使用並不多見,因為你可以用 getOrElse 方法給 Try 提供一個預設值:
val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com")
如果用戶提供的 URL 格式不正確,我們就使用 DuckDuckGo 的 URL 作為備用。
3.2 鏈式操作
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
過濾器和 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 也都值得去看。
文章轉自:https://windor.gitbooks.io/beginners-guide-to-scala/content/chp6-error-handling-with-try.html