函式程式設計就像撰寫一系列代數方程式,而且由於代數沒有 null 值或會擲回例外狀況,因此您不會在 FP 中使用這些功能。這引發了一個有趣的問題:在您可能在 OOP 程式碼中使用 null 值或例外狀況的情況下,您會怎麼做?
Scala 的解決方案是使用 Option
/Some
/None
類別等建構。本課程提供使用這些技術的簡介。
開始之前,有兩點注意事項
Some
和None
類別是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 中常見的解決方案是使用稱為 Option
、Some
和 None
的三個類別。 Some
和 None
類別是 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
中的整數 - 如果這三個字串任何一個都無法轉換成
Int
,y
將會是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)
。
若要查看失敗案例,請將其中一個字串變更為無法轉換成整數的字串。執行此動作後,你會看到 y
是 None
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
類別 - 函數式方法不會擲回例外狀況;它們會回傳值,例如
Option
、Try
或Either
- 處理
Option
值的常見方式是match
和for
表達式 - 選項可以視為一個項目 (
Some
) 和沒有項目的容器 (None
) - 選項也可以用於選擇性建構函式或方法參數