當你在嘗試一門新的語言時,可能不會過於關注程序出錯的問題, 但當真的去創造可用的代碼時,就不能再忽視代碼中的可能產生的錯誤和異常了。 鑑於各類各樣的緣由,人們每每低估了語言對錯誤處理支持程度的重要性。html
事實會代表,Scala 可以很優雅的處理此類問題, 這一部分,我會介紹 Scala 基於 Try 的錯誤處理機制,以及這背後的緣由。 我將使用一個在 Scala 2.10 新引入的特性,該特性向 2.9.3 兼容, 所以,請確保你的 Scala 版本不低於 2.9.3。java
在介紹 Scala 錯誤處理的慣用法以前,咱們先看看其餘語言(如,Java,Ruby)的錯誤處理機制。 和這些語言相似,Scala 也容許你拋出異常:git
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 是表達式(返回一個值),所以下面的代碼會返回異常的消息:apache
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 最好的方式是將它與 Option 做對比。併發
Option[A] 是一個可能有值也可能沒值的容器, Try[A] 則表示一種計算: 這種計算在成功的狀況下,返回類型爲 A 的值,在出錯的狀況下,返回 Throwable 。 這種能夠容納錯誤的容器能夠很輕易的在併發執行的程序之間傳遞。app
Try 有兩個子類型:編程語言
若是知道一個計算可能致使錯誤,咱們能夠簡單的使用 Try[A] 做爲函數的返回類型。 這使得出錯的可能性變得很明確,並且強制客戶端以某種方式處理出錯的可能。ide
假設,須要實現一個簡單的網頁爬取器:用戶可以輸入想爬取的網頁 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 與使用 Option 很是類似,在這裏你看不到太多新的東西。
你能夠調用 isSuccess 方法來檢查一個 Try 是否成功,而後經過 get 方法獲取它的值, 可是,這種方式的使用並很少見,由於你能夠用 getOrElse 方法給 Try 提供一個默認值:
val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com")
若是用戶提供的 URL 格式不正確,咱們就使用 DuckDuckGo 的 URL 做爲備用。
Try 最重要的特徵是,它也支持高階函數,就像 Option 同樣。 在下面的示例中,你將看到,在 Try 上也進行鏈式操做,捕獲可能發生的異常,並且代碼可讀性不錯。
將一個是 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 類型。
既然 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