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

類型 Option

前幾章,我們討論了許多相當(dāng)先進(jìn)的技術(shù),尤其是模式匹配和提取器。 是時(shí)候來看一看 Scala 另一個(gè)基本特性了: Option 類型。

可能你已經(jīng)見過它在 Map API 中的使用;在實(shí)現(xiàn)自己的提取器時(shí),我們也用過它, 然而,它還需要更多的解釋。 你可能會想知道它到底解決什么問題,為什么用它來處理缺失值要比其他方法好, 而且可能你還不知道該怎么在你的代碼中使用它。 這一章的目的就是消除這些問號,并教授你作為一個(gè)新手所應(yīng)該了解的 Option 知識。

基本概念

Java 開發(fā)者一般都知道 NullPointerException(其他語言也有類似的東西), 通常這是由于某個(gè)方法返回了 null ,但這并不是開發(fā)者所希望發(fā)生的,代碼也不好去處理這種異常。

null 通常被濫用來表征一個(gè)可能會缺失的值。 不過,某些語言以一種特殊的方法對待 null 值,或者允許你安全的使用可能是 null 的值。 比如說,Groovy 有 安全運(yùn)算符(Safe Navigation Operator) 用于訪問屬性, 這樣 foo?.bar?.baz 不會在 foobarnull 時(shí)而引發(fā)異常,而是直接返回 null, 然而,Groovy 中沒有什么機(jī)制來強(qiáng)制你使用此運(yùn)算符,所以如果你忘記使用它,那就完蛋了!

Clojure 對待 nil 基本上就像對待空字符串一樣。 也可以把它當(dāng)作列表或者映射表一樣去訪問,這意味著, nil 在調(diào)用層級中向上冒泡。 很多時(shí)候這樣是可行的,但有時(shí)會導(dǎo)致異常出現(xiàn)在更高的調(diào)用層級中,而那里的代碼沒有對 nil 加以考慮。

Scala 試圖通過擺脫 null 來解決這個(gè)問題,并提供自己的類型用來表示一個(gè)值是可選的(有值或無值), 這就是 Option[A] 特質(zhì)。

Option[A] 是一個(gè)類型為 A 的可選值的容器: 如果值存在, Option[A] 就是一個(gè) Some[A] ,如果不存在, Option[A] 就是對象 None 。

在類型層面上指出一個(gè)值是否存在,使用你的代碼的開發(fā)者(也包括你自己)就會被編譯器強(qiáng)制去處理這種可能性, 而不能依賴值存在的偶然性。

Option 是強(qiáng)制的!不要使用 null 來表示一個(gè)值是缺失的。

創(chuàng)建 Option

通常,你可以直接實(shí)例化 Some 樣例類來創(chuàng)建一個(gè) Option 。

val greeting: Option[String] = Some("Hello world")

或者,在知道值缺失的情況下,直接使用 None 對象:

val greeting: Option[String] = None

然而,在實(shí)際工作中,你不可避免的要去操作一些 Java 庫, 或者是其他將 null 作為缺失值的JVM 語言的代碼。 為此, Option 伴生對象提供了一個(gè)工廠方法,可以根據(jù)給定的參數(shù)創(chuàng)建相應(yīng)的 Option

val absentGreeting: Option[String] = Option(null) // absentGreeting will be None
val presentGreeting: Option[String] = Option("Hello!") // presentGreeting will be Some("Hello!")

使用 Option

目前為止,所有的這些都很簡潔,不過該怎么使用 Option 呢?是時(shí)候開始舉些無聊的例子了。

想象一下,你正在為某個(gè)創(chuàng)業(yè)公司工作,要做的第一件事情就是實(shí)現(xiàn)一個(gè)用戶的存儲庫, 要求能夠通過唯一的用戶 ID 來查找他們。 有時(shí)候請求會帶來假的 ID,這種情況,查找方法就需要返回 Option[User] 類型的數(shù)據(jù)。 一個(gè)假想的實(shí)現(xiàn)可能是:

  case class User(
    id: Int,
    firstName: String,
    lastName: String,
    age: Int,
    gender: Option[String]
  )

  object UserRepository {
    private val users = Map(1 -> User(1, "John", "Doe", 32, Some("male")),
                            2 -> User(2, "Johanna", "Doe", 30, None))
    def findById(id: Int): Option[User] = users.get(id)
    def findAll = users.values
  }

現(xiàn)在,假設(shè)從 UserRepository 接收到一個(gè) Option[User] 實(shí)例,并需要拿它做點(diǎn)什么,該怎么辦呢?

一個(gè)辦法就是通過 isDefined 方法來檢查它是否有值。 如果有,你就可以用 get 方法來獲取該值:

  val user1 = UserRepository.findById(1)
  if (user1.isDefined) {
    println(user1.get.firstName)
  } // will print "John"

這和 Guava 庫 中的 Optional 使用方法類似。 不過這種使用方式太過笨重,更重要的是,使用 get 之前, 你可能會忘記用 isDefined 做檢查,這會導(dǎo)致運(yùn)行期出現(xiàn)異常。 這樣一來,相對于 null ,使用 Option 并沒有什么優(yōu)勢。

你應(yīng)該盡可能遠(yuǎn)離這種訪問方式!

提供一個(gè)默認(rèn)值

很多時(shí)候,在值不存在時(shí),需要進(jìn)行回退,或者提供一個(gè)默認(rèn)值。 Scala 為 Option 提供了 getOrElse 方法,以應(yīng)對這種情況:

  val user = User(2, "Johanna", "Doe", 30, None)
  println("Gender: " + user.gender.getOrElse("not specified")) // will print "not specified"

請注意,作為 getOrElse 參數(shù)的默認(rèn)值是一個(gè) 傳名參數(shù) , 這意味著,只有當(dāng)這個(gè) Option 確實(shí)是 None 時(shí),傳名參數(shù)才會被求值。 因此,沒必要擔(dān)心創(chuàng)建默認(rèn)值的代價(jià),它只有在需要時(shí)才會發(fā)生。

模式匹配

Some 是一個(gè)樣例類,可以出現(xiàn)在模式匹配表達(dá)式或者其他允許模式出現(xiàn)的地方。 上面的例子可以用模式匹配來重寫:

  val user = User(2, "Johanna", "Doe", 30, None)
  user.gender match {
    case Some(gender) => println("Gender: " + gender)
    case None => println("Gender: not specified")
  }

或者,你想刪除重復(fù)的 println 語句,并重點(diǎn)突出模式匹配表達(dá)式的使用:

  val user = User(2, "Johanna", "Doe", 30, None)
  val gender = user.gender match {
    case Some(gender) => gender
    case None => "not specified"
  }
  println("Gender: " + gender)

你可能已經(jīng)發(fā)現(xiàn)用模式匹配處理 Option 實(shí)例是非常啰嗦的,這也是它非慣用法的原因。 所以,即使你很喜歡模式匹配,也盡量用其他方法吧。

不過在 Option 上使用模式確實(shí)是有一個(gè)相當(dāng)優(yōu)雅的方式, 在下面的 for 語句一節(jié)中,你就會學(xué)到。

作為集合的 Option

到目前為止,你還沒有看見過優(yōu)雅使用 Option 的方式吧。下面這個(gè)就是了。

前文我提到過, Option 是類型 A 的容器,更確切地說,你可以把它看作是某種集合, 這個(gè)特殊的集合要么只包含一個(gè)元素,要么就什么元素都沒有。

雖然在類型層次上, Option 并不是 Scala 的集合類型, 但,凡是你覺得 Scala 集合好用的方法, Option 也有, 你甚至可以將其轉(zhuǎn)換成一個(gè)集合,比如說 List 。

那么這又能讓你做什么呢?

執(zhí)行一個(gè)副作用

如果想在 Option 值存在的時(shí)候執(zhí)行某個(gè)副作用,foreach 方法就派上用場了:

 UserRepository.findById(2).foreach(user => println(user.firstName)) // prints "Johanna"

如果這個(gè) Option 是一個(gè) Some ,傳遞給 foreach 的函數(shù)就會被調(diào)用一次,且只有一次; 如果是 None ,那它就不會被調(diào)用。

執(zhí)行映射

Option 表現(xiàn)的像集合,最棒的一點(diǎn)是, 你可以用它來進(jìn)行函數(shù)式編程,就像處理列表、集合那樣。

正如你可以將 List[A] 映射到 List[B] 一樣,你也可以映射 Option[A]Option[B]: 如果 Option[A] 實(shí)例是 Some[A] 類型,那映射結(jié)果就是 Some[B] 類型;否則,就是 None

如果將 OptionList 做對比 ,那 None 就相當(dāng)于一個(gè)空列表: 當(dāng)你映射一個(gè)空的 List[A] ,會得到一個(gè)空的 List[B] , 而映射一個(gè)是 NoneOption[A] 時(shí),得到的 Option[B] 也是 None 。

讓我們得到一個(gè)可能不存在的用戶的年齡:

val age = UserRepository.findById(1).map(_.age) // age is Some(32)

Option 與 flatMap

也可以在 gender 上做 map 操作:

val gender = UserRepository.findById(1).map(_.gender) // gender is an Option[Option[String]]

所生成的 gender 類型是 Option[Option[String]] 。這是為什么呢?

這樣想:你有一個(gè)裝有 UserOption 容器,在容器里面,你將 User 映射到 Option[String]User 類上的屬性 genderOption[String] 類型的)。 得到的必然是嵌套的 Option。

既然可以 flatMap 一個(gè) List[List[A]]List[B] , 也可以 flatMap 一個(gè) Option[Option[A]]Option[B] ,這沒有任何問題: Option 提供了 flatMap 方法。

val gender1 = UserRepository.findById(1).flatMap(_.gender) // gender is Some("male")
val gender2 = UserRepository.findById(2).flatMap(_.gender) // gender is None
val gender3 = UserRepository.findById(3).flatMap(_.gender) // gender is None

現(xiàn)在結(jié)果就變成了 Option[String] 類型, 如果 usergender 都有值,那結(jié)果就會是 Some 類型,反之,就得到一個(gè) None 。

要理解這是什么原理,讓我們看看當(dāng) flatMap 一個(gè) List[List[A]] 時(shí),會發(fā)生什么? (要記得, Option 就像一個(gè)集合,比如列表)

val names: List[List[String]] =
 List(List("John", "Johanna", "Daniel"), List(), List("Doe", "Westheide"))
names.map(_.map(_.toUpperCase))
// results in List(List("JOHN", "JOHANNA", "DANIEL"), List(), List("DOE", "WESTHEIDE"))
names.flatMap(_.map(_.toUpperCase))
// results in List("JOHN", "JOHANNA", "DANIEL", "DOE", "WESTHEIDE")

如果我們使用 flatMap ,內(nèi)部列表中的所有元素會被轉(zhuǎn)換成一個(gè)扁平的字符串列表。 顯然,如果內(nèi)部列表是空的,則不會有任何東西留下。

現(xiàn)在回到 Option 類型,如果映射一個(gè)由 Option 組成的列表呢?

val names: List[Option[String]] = List(Some("Johanna"), None, Some("Daniel"))
names.map(_.map(_.toUpperCase)) // List(Some("JOHANNA"), None, Some("DANIEL"))
names.flatMap(xs => xs.map(_.toUpperCase)) // List("JOHANNA", "DANIEL")

如果只是 map ,那結(jié)果類型還是 List[Option[String]] 。 而使用 flatMap 時(shí),內(nèi)部集合的元素就會被放到一個(gè)扁平的列表里: 任何一個(gè) Some[String] 里的元素都會被解包,放入結(jié)果集中; 而原列表中的 None 值由于不包含任何元素,就直接被過濾出去了。

記住這一點(diǎn),然后再去看看 faltMapOption 身上做了什么。

過濾 Option

也可以像過濾列表那樣過濾 Option: 如果選項(xiàng)包含有值,而且傳遞給 filter 的謂詞函數(shù)返回真, filter 會返回 Some 實(shí)例。 否則(即選項(xiàng)沒有值,或者謂詞函數(shù)返回假值),返回值為 None 。

UserRepository.findById(1).filter(_.age > 30) // None, because age is <= 30
UserRepository.findById(2).filter(_.age > 30) // Some(user), because age is > 30
UserRepository.findById(3).filter(_.age > 30) // None, because user is already None

for 語句

現(xiàn)在,你已經(jīng)知道 Option 可以被當(dāng)作集合來看待,并且有 map 、 flatMap 、 filter 這樣的方法。 可能你也在想 Option 是否能夠用在 for 語句中,答案是肯定的。 而且,用 for 語句來處理 Option 是可讀性最好的方式,尤其是當(dāng)你有多個(gè) map 、flatMap 、filter 調(diào)用的時(shí)候。 如果只是一個(gè)簡單的 map 調(diào)用,那 for 語句可能有點(diǎn)繁瑣。

假如我們想得到一個(gè)用戶的性別,可以這樣使用 for 語句:

for {
  user <- UserRepository.findById(1)
  gender <- user.gender
} yield gender // results in Some("male")

可能你已經(jīng)知道,這樣的 for 語句等同于嵌套的 flatMap 調(diào)用。 如果 UserRepository.findById 返回 None,或者 genderNone , 那這個(gè) for 語句的結(jié)果就是 None 。 不過這個(gè)例子里, gender 含有值,所以返回結(jié)果是 Some 類型的。

如果我們想返回所有用戶的性別(當(dāng)然,如果用戶設(shè)置了性別),可以遍歷用戶,yield 其性別:

for {
  user <- UserRepository.findAll
  gender <- user.gender
} yield gender
// result in List("male")

在生成器左側(cè)使用

也許你還記得,前一章曾經(jīng)提到過, for 語句中生成器的左側(cè)也是一個(gè)模式。 這意味著也可以在 for 語句中使用包含選項(xiàng)的模式。

重寫之前的例子:

 for {
   User(_, _, _, _, Some(gender)) <- UserRepository.findAll
 } yield gender

在生成器左側(cè)使用 Some 模式就可以在結(jié)果集中排除掉值為 None 的元素。

鏈接 Option

Option 還可以被鏈接使用,這有點(diǎn)像偏函數(shù)的鏈接: 在 Option 實(shí)例上調(diào)用 orElse 方法,并將另一個(gè) Option 實(shí)例作為傳名參數(shù)傳遞給它。 如果一個(gè) Option 是 None , orElse 方法會返回傳名參數(shù)的值,否則,就直接返回這個(gè) Option。

一個(gè)很好的使用案例是資源查找:對多個(gè)不同的地方按優(yōu)先級進(jìn)行搜索。 下面的例子中,我們首先搜索 config 文件夾,并調(diào)用 orElse 方法,以傳遞備用目錄:

case class Resource(content: String)
val resourceFromConfigDir: Option[Resource] = None
val resourceFromClasspath: Option[Resource] = Some(Resource("I was found on the classpath"))
val resource = resourceFromConfigDir orElse resourceFromClasspath

如果想鏈接多個(gè)選項(xiàng),而不僅僅是兩個(gè),使用 orElse 會非常合適。 不過,如果只是想在值缺失的情況下提供一個(gè)默認(rèn)值,那還是使用 getOrElse 吧。

總結(jié)

在這一章里,你學(xué)到了有關(guān) Option 的所有知識, 這有利于你理解別人的代碼,也有利于你寫出更可讀,更函數(shù)式的代碼。

這一章最重要的一點(diǎn)是:列表、集合、映射、Option,以及之后你會見到的其他數(shù)據(jù)類型, 它們都有一個(gè)非常統(tǒng)一的使用方式,這種使用方式既強(qiáng)大又優(yōu)雅。

下一章,你將學(xué)習(xí) Scala 錯(cuò)誤處理的慣用法。