Scala 3 — 書籍

函式錯誤處理

語言

函式程式設計就像撰寫一系列代數方程式,而且由於代數沒有 null 值或會擲回例外狀況,因此您不會在 FP 中使用這些功能。這引發了一個有趣的問題:在您可能在 OOP 程式碼中使用 null 值或例外狀況的情況下,您會怎麼做?

Scala 的解決方案是使用 Option/Some/None 類別等建構。本課程提供使用這些技術的簡介。

開始之前,有兩點注意事項

  • SomeNone 類別是 Option 的子類別。
  • 與其重複說「選項/部分/」,以下文字通常只提到「選項」或「選項類別」。

第一個範例

雖然這個第一個範例不處理 null 值,但這是介紹選項類別的好方法,所以我們從這裡開始。

想像一下,您想要撰寫一個方法,讓它可以輕鬆地將字串轉換為整數值,而且您想要一個優雅的方法來處理當您的方法取得類似 "Hello" 的字串(而非 "1")時所引發的例外狀況。這種方法的第一個猜測看起來可能像這樣

def makeInt(s: String): Int =
  try {
    Integer.parseInt(s.trim)
  } catch {
    case e: Exception => 0
  }
def makeInt(s: String): Int =
  try
    Integer.parseInt(s.trim)
  catch
    case e: Exception => 0

如果轉換成功,這個方法會傳回正確的 Int 值,但如果失敗,這個方法會傳回 0。這對於某些目的來說可能沒問題,但它並不是很精確。例如,這個方法可能已收到 "0",但它也可能已收到 "foo""bar",或其他會引發例外狀況的無限個字串。這是一個真正問題:您如何知道這個方法真正收到 "0",或收到其他東西?答案是,使用這種方法,沒有辦法知道。

使用選項/部分/無

Scala 中常見的解決方案是使用稱為 OptionSomeNone 的三個類別。 SomeNone 類別是 Option 的子類別,因此解決方案如下運作

  • 宣告 makeInt 傳回 Option 型別
  • 如果 makeInt 收到可以轉換成 Int 的字串,答案會包裝在 Some
  • 如果 makeInt 收到無法轉換的字串,它會傳回 None

以下是 makeInt 的修改版本

def makeInt(s: String): Option[Int] =
  try {
    Some(Integer.parseInt(s.trim))
  } catch {
    case e: Exception => None
  }
def makeInt(s: String): Option[Int] =
  try
    Some(Integer.parseInt(s.trim))
  catch
    case e: Exception => None

這段程式碼可以解讀為:「當給定的字串轉換為整數時,傳回包裝在 Some 中的 Int,例如 Some(1)。當字串無法轉換為整數時,會擲回例外並捕捉,而方法會傳回 None 值。」

這些範例顯示 makeInt 的運作方式

val a = makeInt("1")     // Some(1)
val b = makeInt("one")   // None

如所示,字串 "1" 會產生 Some(1),而字串 "one" 會產生 None。這是 Option 處理錯誤的方法精髓。如所示,此技術用於讓方法可以傳回,而不是例外。在其他情況下,Option 值也用來取代 null 值。

兩則注意事項

  • 您會發現這種方法用於整個 Scala 函式庫類別,以及第三方 Scala 函式庫中。
  • 此範例的一個重點是,函式方法不會擲回例外狀況;而是傳回值,例如 Option

成為 makeInt 的使用者

現在想像您是 makeInt 方法的使用者。您知道它傳回 Option[Int] 的子類別,因此問題變成,您要如何使用這些傳回類型?

有兩個常見的答案,視您的需求而定

  • 使用 match 表達式
  • 使用 for 表達式

使用 match 表達式

一個可能的解決方案是使用 match 表達式

makeInt(x) match {
  case Some(i) => println(i)
  case None => println("That didn’t work.")
}
makeInt(x) match
  case Some(i) => println(i)
  case None => println("That didn’t work.")

在此範例中,如果 x 可以轉換成 Int,則會評估第一個 case 子句右側的表達式;如果 x 無法轉換成 Int,則會評估第二個 case 子句右側的表達式。

使用 for 表達式

另一個常見的解決方案是使用 for 表達式,也就是本書稍早顯示的 for/yield 組合。例如,想像您想要將三個字串轉換成整數值,然後將它們加總。以下是使用 for 表達式和 makeInt 執行此動作的方式

val y = for {
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
} yield {
  a + b + c
}
val y = for
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
yield
  a + b + c

該表達式執行後,y 將會是以下兩種情況之一

  • 如果這三個字串全部都轉換成 Int 值,y 將會是 Some[Int],也就是一個包在 Some 中的整數
  • 如果這三個字串任何一個都無法轉換成 Inty 將會是 None

你可以自己測試看看

val stringA = "1"
val stringB = "2"
val stringC = "3"

val y = for {
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
} yield {
  a + b + c
}
val stringA = "1"
val stringB = "2"
val stringC = "3"

val y = for 
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
yield
  a + b + c

使用該範例資料,變數 y 將會是值 Some(6)

若要查看失敗案例,請將其中一個字串變更為無法轉換成整數的字串。執行此動作後,你會看到 yNone

y: Option[Int] = None

將 Option 視為一個容器

心智模式通常有助於我們了解新的情況,因此如果你不熟悉 Option 類別,可以將它們視為一個容器

  • Some 是只有一個項目在其中的容器
  • None 是容器,但其中沒有任何項目

如果你比較喜歡將 Option 類別視為一個盒子,None 就如同一個空的盒子。它原本可以放一些東西,但現在沒有。

使用 Option 取代 null

回到 null 值,一個 null 值可能在不知不覺中潛入程式碼的地方,就是像這樣的類別

class Address(
  var street1: String,
  var street2: String,
  var city: String,
  var state: String,
  var zip: String
)

雖然地球上的每個地址都有 street1 值,但 street2 值是可選的。因此,street2 欄位可以指定為 null

val santa = new Address(
  "1 Main Street",
  null,               // <-- D’oh! A null value!
  "North Pole",
  "Alaska",
  "99705"
)
val santa = Address(
  "1 Main Street",
  null,               // <-- D’oh! A null value!
  "North Pole",
  "Alaska",
  "99705"
)

過去,開發人員會在這種情況下使用空白字串和 null 值,這兩種方法都是為了解決根本問題的權宜之計:street2可選欄位。在 Scala(和其他現代語言)中,正確的解決方案是預先宣告 street2 是可選的

class Address(
  var street1: String,
  var street2: Option[String],   // an optional value
  var city: String, 
  var state: String, 
  var zip: String
)

現在開發人員可以撰寫更精確的程式碼,例如

val santa = new Address(
  "1 Main Street",
  None,           // 'street2' has no value
  "North Pole",
  "Alaska",
  "99705"
)
val santa = Address(
  "1 Main Street",
  None,           // 'street2' has no value
  "North Pole",
  "Alaska",
  "99705"
)

或這樣

val santa = new Address(
  "123 Main Street",
  Some("Apt. 2B"),
  "Talkeetna",
  "Alaska",
  "99676"
)
val santa = Address(
  "123 Main Street",
  Some("Apt. 2B"),
  "Talkeetna",
  "Alaska",
  "99676"
)

Option 不是唯一的解決方案

雖然本節重點在於 Option 類別,但 Scala 還有其他一些替代方案。

例如,稱為 Try/Success/Failure 的三個類別以相同的方式運作,但 (a) 只有在程式碼可能會擲出例外時才會使用這些類別,以及 (b) 你希望使用 Failure 類別,因為它讓你能夠存取例外訊息。例如,這些 Try 類別通常用於撰寫與檔案、資料庫和網路服務互動的方法,因為這些函式可以輕鬆擲出例外。

快速回顧

本節很長,讓我們快速回顧一下

  • 函式程式設計師不使用 null
  • 替換 null 值的一個主要方式是使用 Option 類別
  • 函數式方法不會擲回例外狀況;它們會回傳值,例如 OptionTryEither
  • 處理 Option 值的常見方式是 matchfor 表達式
  • 選項可以視為一個項目 (Some) 和沒有項目的容器 (None)
  • 選項也可以用於選擇性建構函式或方法參數

此頁面的貢獻者