鍍金池/ 教程/ Scala/ Try 與錯(cuò)誤處理
高階函數(shù)與 DRY
序列提取
譯者結(jié)語(yǔ)
類型 Future
類型 Option
Scala 初學(xué)指南
類型類
模式匹配與匿名函數(shù)
路徑依賴類型
提取器
類型 Either
Try 與錯(cuò)誤處理
介紹
實(shí)戰(zhàn)中的 Promise 和 Future
柯里化和部分函數(shù)應(yīng)用
無(wú)處不在的模式

Try 與錯(cuò)誤處理

當(dāng)你在嘗試一門新的語(yǔ)言時(shí),可能不會(huì)過(guò)于關(guān)注程序出錯(cuò)的問(wèn)題, 但當(dāng)真的去創(chuàng)造可用的代碼時(shí),就不能再忽視代碼中的可能產(chǎn)生的錯(cuò)誤和異常了。 鑒于各種各樣的原因,人們往往低估了語(yǔ)言對(duì)錯(cuò)誤處理支持程度的重要性。

事實(shí)會(huì)表明,Scala 能夠很優(yōu)雅的處理此類問(wèn)題, 這一部分,我會(huì)介紹 Scala 基于 Try 的錯(cuò)誤處理機(jī)制,以及這背后的原因。 我將使用一個(gè)在 Scala 2.10 新引入的特性,該特性向 2.9.3 兼容, 因此,請(qǐng)確保你的 Scala 版本不低于 2.9.3。

異常的拋出和捕獲

在介紹 Scala 錯(cuò)誤處理的慣用法之前,我們先看看其他語(yǔ)言(如,Java,Ruby)的錯(cuò)誤處理機(jī)制。 和這些語(yǔ)言類似,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 中的方式被捕獲,雖然是使用偏函數(shù)來(lái)指定要處理的異常類型。 此外,Scala 的 try/catch 是表達(dá)式(返回一個(gè)值),因此下面的代碼會(huì)返回異常的消息:

val youngCustomer = Customer(15)
try {
  buyCigarettes(youngCustomer)
  "Yo, here are your cancer sticks! Happy smokin'!"
} catch {
    case UnderAgeException(msg) => msg
}

函數(shù)式的錯(cuò)誤處理

現(xiàn)在,如果代碼中到處是上面的異常處理代碼,那它很快就會(huì)變得丑陋無(wú)比,和函數(shù)式程序設(shè)計(jì)非常不搭。 對(duì)于高并發(fā)應(yīng)用來(lái)說(shuō),這也是一個(gè)很差勁的解決方式,比如, 假設(shè)需要處理在其他線程執(zhí)行的 actor 所引發(fā)的異常,顯然你不能用捕獲異常這種處理方式, 你可能會(huì)想到其他解決方案,例如去接收一個(gè)表示錯(cuò)誤情況的消息。

一般來(lái)說(shuō),在 Scala 中,好的做法是通過(guò)從函數(shù)里返回一個(gè)合適的值來(lái)通知人們程序出錯(cuò)了。 別擔(dān)心,我們不會(huì)回到 C 中那種需要使用按約定進(jìn)行檢查的錯(cuò)誤編碼的錯(cuò)誤處理。 相反,Scala 使用一個(gè)特定的類型來(lái)表示可能會(huì)導(dǎo)致異常的計(jì)算,這個(gè)類型就是 Try。

Try 的語(yǔ)義

解釋 Try 最好的方式是將它與上一章所講的 Option 作對(duì)比。

Option[A] 是一個(gè)可能有值也可能沒(méi)值的容器, Try[A] 則表示一種計(jì)算: 這種計(jì)算在成功的情況下,返回類型為 A 的值,在出錯(cuò)的情況下,返回 Throwable 。 這種可以容納錯(cuò)誤的容器可以很輕易的在并發(fā)執(zhí)行的程序之間傳遞。

Try 有兩個(gè)子類型:

  1. Success[A]:代表成功的計(jì)算。
  2. 封裝了 ThrowableFailure[A]:代表出了錯(cuò)的計(jì)算。

如果知道一個(gè)計(jì)算可能導(dǎo)致錯(cuò)誤,我們可以簡(jiǎn)單的使用 Try[A] 作為函數(shù)的返回類型。 這使得出錯(cuò)的可能性變得很明確,而且強(qiáng)制客戶端以某種方式處理出錯(cuò)的可能。

假設(shè),需要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的網(wǎng)頁(yè)爬取器:用戶能夠輸入想爬取的網(wǎng)頁(yè) URL, 程序就需要去分析 URL 輸入,并從中創(chuàng)建一個(gè) java.net.URL

import scala.util.Try
import java.net.URL
def parseURL(url: String): Try[URL] = Try(new URL(url))

正如你所看到的,函數(shù)返回類型為 Try[URL]: 如果給定的 url 語(yǔ)法正確,這將是 Success[URL], 否則, URL 構(gòu)造器會(huì)引發(fā) MalformedURLException ,從而返回值變成 Failure[URL] 類型。

上例中,我們還用了 Try 伴生對(duì)象里的 apply 工廠方法,這個(gè)方法接受一個(gè)類型為 A傳名參數(shù), 這意味著, new URL(url) 是在 Tryapply 方法里執(zhí)行的。

apply 方法不會(huì)捕獲任何非致命的異常,僅僅返回一個(gè)包含相關(guān)異常的 Failure 實(shí)例。

因此, parseURL("http://danielwestheide.com") 會(huì)返回一個(gè) Success[URL] ,包含了解析后的網(wǎng)址, 而 parseULR("garbage") 將返回一個(gè)含有 MalformedURLExceptionFailure[URL]。

使用 Try

使用 Try 與使用 Option 非常相似,在這里你看不到太多新的東西。

你可以調(diào)用 isSuccess 方法來(lái)檢查一個(gè) Try 是否成功,然后通過(guò) get 方法獲取它的值, 但是,這種方式的使用并不多見(jiàn),因?yàn)槟憧梢杂?getOrElse 方法給 Try 提供一個(gè)默認(rèn)值:

val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com")

如果用戶提供的 URL 格式不正確,我們就使用 DuckDuckGo 的 URL 作為備用。

鏈?zhǔn)讲僮?/h4>

Try 最重要的特征是,它也支持高階函數(shù),就像 Option 一樣。 在下面的示例中,你將看到,在 Try 上也進(jìn)行鏈?zhǔn)讲僮鳎东@可能發(fā)生的異常,而且代碼可讀性不錯(cuò)。

Mapping 和 Flat Mapping

將一個(gè)是 Success[A]Try[A] 映射到 Try[B] 會(huì)得到 Success[B] 。 如果它是 Failure[A] ,就會(huì)得到 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)

如果鏈接多個(gè) map 操作,會(huì)產(chǎn)生嵌套的 Try 結(jié)構(gòu),這并不是我們想要的。 考慮下面這個(gè)返回輸入流的方法:

import java.io.InputStream
def inputStreamForURL(url: String): Try[Try[Try[InputStream]]] ` parseURL(url).map { u `>
 Try(u.openConnection()).map(conn => Try(conn.getInputStream))
}

由于每個(gè)傳遞給 map 的匿名函數(shù)都返回 Try,因此返回類型就變成了 Try[Try[Try[InputStream]]] 。

這時(shí)候, flatMap 就派上用場(chǎng)了。 Try[A] 上的 flatMap 方法接受一個(gè)映射函數(shù),這個(gè)函數(shù)類型是 (A) => Try[B]。 如果我們的 Try[A] 已經(jīng)是 Failure[A] 了,那么里面的異常就直接被封裝成 Failure[B] 返回, 否則, flatMapSuccess[A] 里面的值解包出來(lái),并通過(guò)映射函數(shù)將其映射到 Try[B]

這意味著,我們可以通過(guò)鏈接任意個(gè) flatMap 調(diào)用來(lái)創(chuàng)建一條操作管道,將值封裝在 Success 里一層層的傳遞。

現(xiàn)在讓我們用 flatMap 來(lái)重寫先前的例子:

def inputStreamForURL(url: String): Try[InputStream] =
 parseURL(url).flatMap { u =>
   Try(u.openConnection()).flatMap(conn => Try(conn.getInputStream))
 }

這樣,我們就得到了一個(gè) Try[InputStream], 它可以是一個(gè) Failure,包含了在 flatMap 過(guò)程中可能出現(xiàn)的異常; 也可以是一個(gè) Success,包含了最后的結(jié)果。

過(guò)濾器和 foreach

當(dāng)然,你也可以對(duì) Try 進(jìn)行過(guò)濾,或者調(diào)用 foreach ,既然已經(jīng)學(xué)過(guò) Option,對(duì)于這兩個(gè)方法也不會(huì)陌生。

當(dāng)一個(gè) Try 已經(jīng)是 Failure 了,或者傳遞給它的謂詞函數(shù)返回假值,filter 就返回 Failure (如果是謂詞函數(shù)返回假值,那 Failure 里包含的異常是 NoSuchException ), 否則的話, filter 就返回原本的那個(gè) Success ,什么都不會(huì)變:

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]

當(dāng)一個(gè) Try 是 Success 時(shí), foreach 允許你在被包含的元素上執(zhí)行副作用, 這種情況下,傳遞給 foreach 的函數(shù)只會(huì)執(zhí)行一次,畢竟 Try 里面只有一個(gè)元素:

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

當(dāng) Try 是 Failure 時(shí), foreach 不會(huì)執(zhí)行,返回 Unit 類型。

for 語(yǔ)句中的 Try

既然 Try 支持 flatMap 、 map 、 filter ,能夠使用 for 語(yǔ)句也是理所當(dāng)然的事情, 而且這種情況下的代碼更可讀。 為了證明這一點(diǎn),我們來(lái)實(shí)現(xiàn)一個(gè)返回給定 URL 的網(wǎng)頁(yè)內(nèi)容的函數(shù):

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()

這個(gè)方法中,有三個(gè)可能會(huì)出錯(cuò)的地方,但都被 Try 給涵蓋了。 第一個(gè)是我們已經(jīng)實(shí)現(xiàn)的 parseURL 方法, 只有當(dāng)它是一個(gè) Success[URL] 時(shí),我們才會(huì)嘗試打開連接,從中創(chuàng)建一個(gè)新的 InputStream 。 如果這兩步都成功了,我們就 yield 出網(wǎng)頁(yè)內(nèi)容,得到的結(jié)果是 Try[Iterator[String]] 。

當(dāng)然,你可以使用 Source#fromURL 簡(jiǎn)化這個(gè)代碼,并且,這個(gè)代碼最后沒(méi)有關(guān)閉輸入流, 這都是為了保持例子的簡(jiǎn)單性,專注于要講述的主題。

在這個(gè)例子中,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() 可以關(guān)閉輸入流。

模式匹配

代碼往往需要知道一個(gè) Try 實(shí)例是 Success 還是 Failure,這時(shí)候,你應(yīng)該想到模式匹配, 也幸好, SuccessFailure 都是樣例類。

接著上面的例子,如果網(wǎng)頁(yè)內(nèi)容能順利提取到,我們就展示它,否則,打印一個(gè)錯(cuò)誤信息:

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}")
}

從故障中恢復(fù)

如果想在失敗的情況下執(zhí)行某種動(dòng)作,沒(méi)必要去使用 getOrElse, 一個(gè)更好的選擇是 recover ,它接受一個(gè)偏函數(shù),并返回另一個(gè) Try。 如果 recover 是在 Success 實(shí)例上調(diào)用的,那么就直接返回這個(gè)實(shí)例,否則就調(diào)用偏函數(shù)。 如果偏函數(shù)為給定的 Failure 定義了處理動(dòng)作, recover 會(huì)返回 Success ,里面包含偏函數(shù)運(yùn)行得出的結(jié)果。

下面是應(yīng)用了 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!")
}

現(xiàn)在,我們可以在返回值 content 上安全的使用 get 方法了,因?yàn)樗欢ㄊ且粋€(gè) Success。 調(diào)用 content.get.foreach(println) 會(huì)打印 Please make sure to enter a valid URL。

總結(jié)

Scala 的錯(cuò)誤處理和其他范式的編程語(yǔ)言有很大的不同。 Try 類型可以讓你將可能會(huì)出錯(cuò)的計(jì)算封裝在一個(gè)容器里,并優(yōu)雅的去處理計(jì)算得到的值。 并且可以像操作集合和 Option 那樣統(tǒng)一的去操作 Try。

Try 還有其他很多重要的方法,鑒于篇幅限制,這一章并沒(méi)有全部列出,比如 orElse 方法, transformrecoverWith 也都值得去看。

下一章,我們會(huì)探討 Either,另外一種可以代表計(jì)算的類型,但它的可使用范圍要比 Try 大的多。