[轉] Scala Try 與錯誤處理

一.概述

當你在嘗試一門新的語言時,可能不會過於關注程序出錯的問題, 但當真的去創造可用的代碼時,就不能再忽視代碼中的可能產生的錯誤和異常了。 鑑於各類各樣的緣由,人們每每低估了語言對錯誤處理支持程度的重要性。html

事實會代表,Scala 可以很優雅的處理此類問題, 這一部分,我會介紹 Scala 基於 Try 的錯誤處理機制,以及這背後的緣由。 我將使用一個在 Scala 2.10 新引入的特性,該特性向 2.9.3 兼容, 所以,請確保你的 Scala 版本不低於 2.9.3。java

二.異常拋出與捕獲

2.1 其餘語言的錯誤處理機制

在介紹 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
}

2.2 函數式的錯誤處理

如今,若是代碼中處處是上面的異常處理代碼,那它很快就會變得醜陋無比,和函數式程序設計很是不搭。 對於高併發應用來講,這也是一個不好勁的解決方式,好比, 假設須要處理在其餘線程執行的 actor 所引起的異常,顯然你不能用捕獲異常這種處理方式, 你可能會想到其餘解決方案,例如去接收一個表示錯誤狀況的消息。編程

通常來講,在 Scala 中,好的作法是經過從函數裏返回一個合適的值來通知人們程序出錯了。 別擔憂,咱們不會回到 C 中那種須要使用按約定進行檢查的錯誤編碼的錯誤處理。 相反,Scala 使用一個特定的類型來表示可能會致使異常的計算,這個類型就是 Try。安全

Try 的語義

解釋 Try 最好的方式是將它與 Option 做對比。併發

Option[A] 是一個可能有值也可能沒值的容器, Try[A] 則表示一種計算: 這種計算在成功的狀況下,返回類型爲 A 的值,在出錯的狀況下,返回 Throwable 。 這種能夠容納錯誤的容器能夠很輕易的在併發執行的程序之間傳遞。app

Try 有兩個子類型:編程語言

  • Success[A]:表明成功的計算。
  • 封裝了 Throwable 的 Failure[A]:表明出了錯的計算。

若是知道一個計算可能致使錯誤,咱們能夠簡單的使用 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) 是在 Tryapply 方法裏執行的。

apply 方法會捕獲任何非致命的異常,返回一個包含相關異常的 Failure 實例。

所以, parseURL("http://danielwestheide.com") 會返回一個 Success[URL] ,包含了解析後的網址, 而 parseULR("garbage") 將返回一個含有 MalformedURLExceptionFailure[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] 返回, 不然, flatMapSuccess[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]

當一個 TrySuccess 時, foreach 容許你在被包含的元素上執行反作用, 這種狀況下,傳遞給 foreach 的函數只會執行一次,畢竟 Try 裏面只有一個元素:

parseHttpURL("http://danielwestheide.com").foreach(println)

當 Try 是 Failure 時, foreach 不會執行,返回 Unit 類型。

for 語句中的 Try

既然 Try 支持 flatMapmapfilter ,可以使用 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,這時候,你應該想到模式匹配, 也幸虧, SuccessFailure 都是樣例類。

接着上面的例子,若是網頁內容能順利提取到,咱們就展現它,不然,打印一個錯誤信息:

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 方法, transformrecoverWith 也都值得去看。

文章轉自:https://windor.gitbooks.io/beginners-guide-to-scala/content/chp6-error-handling-with-try.html

相關文章
相關標籤/搜索