Scala Try 與錯誤處理

来源:https://www.cnblogs.com/listenfwind/archive/2018/11/01/9892614.html
-Advertisement-
Play Games

一.概述 當你在嘗試一門新的語言時,可能不會過於關註程式出錯的問題, 但當真的去創造可用的代碼時,就不能再忽視代碼中的可能產生的錯誤和異常了。 鑒於各種各樣的原因,人們往往低估了語言對錯誤處理支持程度的重要性。 事實會表明,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) 是在 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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # 生成器:# 生成器實質就是迭代器(省記憶體 惰性機制 只往前)# 1. 通過生成器函數# 2. 通過各種推導式來實現生成器 # def func():# yield# # g = func() - 得到生成 1 # 生成器函數 就是把return 換成yield 2 # return 換成 yie... ...
  • 介面類型探測:類型斷言 介面實例中存儲了實現介面的類型實例,類型的實例有兩種:值類型實例和指針類型實例。在程式運行過程中,介面實例存儲的實例類型可能會動態改變。例如: 所以,需要一種探測介面實例所存儲的是值類型還是指針類型。 探測的方法是: 和`ins.( Type)`。它們有兩個返回值,第二個返回 ...
  • Strom框架基本概念就不提了,這裡主要講的是`Stream`自定義ID的消息流。預設spout、bolt都需實現介面方法`declareOutputFields`,這種情況下發的消息會被所有定義的bolts接收。我們如果需要根據得到的消息類型來選擇不同的bolt,就需要用到Stream Group... ...
  • 今天在工作中遇到的幾個小問題,總結一下: 1.因為業務需要調用PHP的介面,獲取到的返回體需要做一段邏輯處理,然而某個欄位接收到的參數是io.serializable類型,欄位的類型不是預期的string類型,當時有點懵逼,因為是用Scala的match case做模式匹配,也沒多想,幸虧同事提示一 ...
  • Storm框架主要分三個Component:topology,spout,bolt。unconfirmedMap對象存儲了MQ所有發射出去等待確認的消息唯一標識deliveryTag,當storm系統回調ack、fail方法後進行MQ消息的成功確認或失敗重回隊列操作(Storm系統回調方法會在bol... ...
  • 由於筆者在自己設計CRC模塊時遇到很多問題,在網上並未找到一篇具有實際指導意義的文章,在經過多次模擬修改再模擬之後得到了正確的結果,故願意在本文中為大家提供整個設計流程供大家快速完成設計。本文章主要針對具體的實際應用給出一套親測可行的實現辦法,給出設計代碼並提供模擬結果,供各位參考。 一.CRC概述 ...
  • 多線程 thread_local 類型 thread_local變數是C++ 11新引入的一種存儲類型。 thread_local關鍵字修飾的變數具有線程周期(thread duration), 這些變數(或者說對象)線上程開始的時候被生成(allocated), 線上程結束的時候被銷毀(deall ...
  • 一、列表推導式 寫點:[結果 for 變數 in 可迭代對象 if 判斷] 二、字典推導式 寫法:[結果 for 變數 in 可迭代對象 if 判斷] 三、集合推導式 寫法:[結果 for 變數 in 可迭代對象 if 判斷] 結論: 推導式比較耗記憶體。一次載入。而生成器表達式幾乎不占用記憶體。使用的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...