Scala 初學者指南 類型Option學習


類型 Option

前幾章,咱們討論了許多至關先進的技術,尤爲是模式匹配和提取器。 是時候來看一看 Scala 另外一個基本特性了: Option 類型。html

可能你已經見過它在 Map API 中的使用;在實現本身的提取器時,咱們也用過它, 然而,它還須要更多的解釋。 你可能會想知道它到底解決什麼問題,爲何用它來處理缺失值要比其餘方法好, 並且可能你還不知道該怎麼在你的代碼中使用它。 這一章的目的就是消除這些問號,並教授你做爲一個新手所應該瞭解的Option 知識。git

基本概念

Java 開發者通常都知道 NullPointerException(其餘語言也有相似的東西), 一般這是因爲某個方法返回了 null ,但這並非開發者所但願發生的,代碼也很差去處理這種異常。web

值 null 一般被濫用來表徵一個可能會缺失的值。 不過,某些語言以一種特殊的方法對待 null 值,或者容許你安全的使用多是 null 的值。 好比說,Groovy 有 安全運算符(Safe Navigation Operator) 用於訪問屬性, 這樣 foo?.bar?.baz 不會在 foo 或 bar 是 null 時而引起異常,而是直接返回null, 然而,Groovy 中沒有什麼機制來強制你使用此運算符,因此若是你忘記使用它,那就完蛋了!編程

Clojure 對待 nil 基本上就像對待空字符串同樣。 也能夠把它看成列表或者映射表同樣去訪問,這意味着, nil 在調用層級中向上冒泡。 不少時候這樣是可行的,但有時會致使異常出如今更高的調用層級中,而那裏的代碼沒有對 nil 加以考慮。安全

Scala 試圖經過擺脫 null 來解決這個問題,並提供本身的類型用來表示一個值是可選的(有值或無值), 這就是 Option[A] 特質。ide

Option[A] 是一個類型爲 A 的可選值的容器: 若是值存在, Option[A] 就是一個 Some[A] ,若是不存在, Option[A] 就是對象 None 。函數式編程

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

Option 是強制的!不要使用 null 來表示一個值是缺失的。學習

建立 Option

一般,你能夠直接實例化 Some 樣例類來建立一個 Option 。ui

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

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

val greeting: Option[String] = None

然而,在實際工做中,你不可避免的要去操做一些 Java 庫, 或者是其餘將 null 做爲缺失值的JVM 語言的代碼。 爲此, Option 伴生對象提供了一個工廠方法,能夠根據給定的參數建立相應的 Option :

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

使用 Option

目前爲止,全部的這些都很簡潔,不過該怎麼使用 Option 呢?是時候開始舉些無聊的例子了。

想象一下,你正在爲某個創業公司工做,要作的第一件事情就是實現一個用戶的存儲庫, 要求可以經過惟一的用戶 ID 來查找他們。 有時候請求會帶來假的 ID,這種狀況,查找方法就須要返回 Option[User] 類型的數據。 一個假想的實現多是:

  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
  }

如今,假設從 UserRepository 接收到一個 Option[User] 實例,並須要拿它作點什麼,該怎麼辦呢?

一個辦法就是經過 isDefined 方法來檢查它是否有值。 若是有,你就能夠用 get 方法來獲取該值:

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

這和 Guava 庫 中的 Optional 使用方法相似。 不過這種使用方式太過笨重,更重要的是,使用 get 以前, 你可能會忘記用 isDefined 作檢查,這會致使運行期出現異常。 這樣一來,相對於 null ,使用Option 並無什麼優點。

你應該儘量遠離這種訪問方式!

提供一個默認值

不少時候,在值不存在時,須要進行回退,或者提供一個默認值。 Scala 爲 Option 提供了 getOrElse方法,以應對這種狀況:

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

請注意,做爲 getOrElse 參數的默認值是一個 傳名參數 , 這意味着,只有當這個 Option 確實是None 時,傳名參數纔會被求值。 所以,不必擔憂建立默認值的代價,它只有在須要時纔會發生。

模式匹配

Some 是一個樣例類,能夠出如今模式匹配表達式或者其餘容許模式出現的地方。 上面的例子能夠用模式匹配來重寫:

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

或者,你想刪除重複的 println 語句,並重點突出模式匹配表達式的使用:

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

你可能已經發現用模式匹配處理 Option 實例是很是囉嗦的,這也是它非慣用法的緣由。 因此,即便你很喜歡模式匹配,也儘可能用其餘方法吧。

不過在 Option 上使用模式確實是有一個至關優雅的方式, 在下面的 for 語句一節中,你就會學到。

做爲集合的 Option

到目前爲止,你尚未看見過優雅使用 Option 的方式吧。下面這個就是了。

前文我提到過, Option 是類型 A 的容器,更確切地說,你能夠把它看做是某種集合, 這個特殊的集合要麼只包含一個元素,要麼就什麼元素都沒有。

雖然在類型層次上, Option 並非 Scala 的集合類型, 但,凡是你以爲 Scala 集合好用的方法,Option 也有, 你甚至能夠將其轉換成一個集合,好比說 List 。

那麼這又能讓你作什麼呢?

執行一個反作用

若是想在 Option 值存在的時候執行某個反作用,foreach 方法就派上用場了:

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

若是這個 Option 是一個 Some ,傳遞給 foreach 的函數就會被調用一次,且只有一次; 若是是 None,那它就不會被調用。

執行映射

Option 表現的像集合,最棒的一點是, 你能夠用它來進行函數式編程,就像處理列表、集合那樣。

正如你能夠將 List[A] 映射到 List[B] 同樣,你也能夠映射 Option[A] 到 Option[B]: 若是Option[A] 實例是 Some[A] 類型,那映射結果就是 Some[B] 類型;不然,就是 None 。

若是將 Option 和 List 作對比 ,那 None 就至關於一個空列表: 當你映射一個空的 List[A] ,會獲得一個空的 List[B] , 而映射一個是 None 的 Option[A] 時,獲得的 Option[B] 也是 None 。

讓咱們獲得一個可能不存在的用戶的年齡:

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]] 。這是爲何呢?

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

既然能夠 flatMap 一個 List[List[A]] 到 List[B] , 也能夠 flatMap 一個 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 Noneval gender3 = UserRepository.findById(3).flatMap(_.gender) // gender is None

如今結果就變成了 Option[String] 類型, 若是 user 和 gender 都有值,那結果就會是 Some 類型,反之,就獲得一個 None 。

要理解這是什麼原理,讓咱們看看當 flatMap 一個 List[List[A]] 時,會發生什麼? (要記得, Option 就像一個集合,好比列表)

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 ,內部列表中的全部元素會被轉換成一個扁平的字符串列表。 顯然,若是內部列表是空的,則不會有任何東西留下。

如今回到 Option 類型,若是映射一個由 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 ,那結果類型仍是 List[Option[String]] 。 而使用 flatMap 時,內部集合的元素就會被放到一個扁平的列表裏: 任何一個 Some[String] 裏的元素都會被解包,放入結果集中; 而原列表中的 None 值因爲不包含任何元素,就直接被過濾出去了。

記住這一點,而後再去看看 faltMap 在 Option 身上作了什麼。

過濾 Option

也能夠像過濾列表那樣過濾 Option: 若是選項包含有值,並且傳遞給 filter 的謂詞函數返回真,filter 會返回 Some 實例。 不然(即選項沒有值,或者謂詞函數返回假值),返回值爲 None 。

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

for 語句

如今,你已經知道 Option 能夠被看成集合來看待,而且有 map 、 flatMap 、 filter 這樣的方法。 可能你也在想 Option 是否可以用在 for 語句中,答案是確定的。 並且,用 for 語句來處理 Option 是可讀性最好的方式,尤爲是當你有多個 map 、flatMap 、filter 調用的時候。 若是隻是一個簡單的 map調用,那 for 語句可能有點繁瑣。

假如咱們想獲得一個用戶的性別,能夠這樣使用 for 語句:

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

可能你已經知道,這樣的 for 語句等同於嵌套的 flatMap 調用。 若是 UserRepository.findById 返回None,或者 gender 是 None , 那這個 for 語句的結果就是 None 。 不過這個例子裏, gender 含有值,因此返回結果是 Some 類型的。

若是咱們想返回全部用戶的性別(固然,若是用戶設置了性別),能夠遍歷用戶,yield 其性別:

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

在生成器左側使用

也許你還記得,前一章曾經提到過, for 語句中生成器的左側也是一個模式。 這意味着也能夠在 for 語句中使用包含選項的模式。

重寫以前的例子:

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

在生成器左側使用 Some 模式就能夠在結果集中排除掉值爲 None 的元素。

連接 Option

Option 還能夠被連接使用,這有點像偏函數的連接: 在 Option 實例上調用 orElse 方法,並將另外一個 Option 實例做爲傳名參數傳遞給它。 若是一個 Option 是 None , orElse 方法會返回傳名參數的值,不然,就直接返回這個 Option。

一個很好的使用案例是資源查找:對多個不一樣的地方按優先級進行搜索。 下面的例子中,咱們首先搜索config 文件夾,並調用 orElse 方法,以傳遞備用目錄:

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

若是想連接多個選項,而不只僅是兩個,使用 orElse 會很是合適。 不過,若是隻是想在值缺失的狀況下提供一個默認值,那仍是使用 getOrElse 吧。

總結

在這一章裏,你學到了有關 Option 的全部知識, 這有利於你理解別人的代碼,也有利於你寫出更可讀,更函數式的代碼。

這一章最重要的一點是:列表、集合、映射、Option,以及以後你會見到的其餘數據類型, 它們都有一個很是統一的使用方式,這種使用方式既強大又優雅。

下一章,你將學習 Scala 錯誤處理的慣用法。

英文原文:

http://danielwestheide.com/blog/2013/01/09/the-neophytes-guide-to-scala-part-8-welcome-to-the-future.html

相關文章
相關標籤/搜索