[轉] Scala Try 與錯誤處理

zzzzMing發表於2018-11-01

一.概述

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

事實會表明,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) 是在 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

相關文章