Scala Tutorial(9)Option[T] 簡介 by brianhsu | CodeData
top

Scala Tutorial(9)Option[T] 簡介

分享:

Scala Tutorial(8)Tuple 簡介 << 前情

上一篇當中,我們介紹了 Scala 內建的 Tuple 資料型別以及如何透過 Tuple 來表示一組介單的資料。不過想要徹底了解 Scala 提供的控制結構功能,除了 Tuple 之外還有很重要的另一個資料型別需要介紹--Option[T]。

不過在開始正式介紹 Option[T] 之前,要先稍微提一下,Option[T] 是裡的 [T] 是 Scala 裡的汎型(Generic)語法,若您有 Java 的基礎的話,Option[T] 在 Java 中會寫成 Option,兩者的意義是相同的。若您沒有 Java 基礎,也不曉得汎型是什麼東西也不要緊,接下來的文章裡會詳細說明這裡的 [T] 是做什麼用的。

Option[T] 想要解決的問題

我們之前看到,Tuple 可以幫助我們快速地定義一組資料。同樣的,我們也要問:如果 Option[T] 在 Scala 中這麼重要,那這樣的資料型別可以幫我們解決什麼樣的問題?他比起原本我們習慣的做法有什麼優點?

那麼 Option[T] 到底想要解決什麼樣的問題呢?其實答案很簡單--讓我們可以知道某個可能沒有值的東西到底有沒有值。

這樣子的講法很像很饒舌,不過我們來看看下面這幾個 Java 程式的 API 吧:

public String getFirstNameFromDB()
public String getMiddleNameFromDB()
public String getLastNameFromDB()

很明顯的,我們從函式的名稱可以猜到這應該是從資料庫裡把人名取出來用的,而且看起來是有做錯誤處理,所以這幾個函式不會丟出 Exception。很好,所以接下來我們可以寫出像下面的函式,來取出某個人的全名,並且將其第一個字母轉為大寫:

public String capitalize(String line) {
    return Character.toUpperCase(line.charAt(0)) + line.substring(1);
}

public void getCapitalizeName() {
    return capitalize(getFirstNameFromDB()) + " "
           capitalize(getMiddleNameFromDB()) + " "
           capitalize(getLastNameFromDB()); 
}

類似這樣的程式碼看似沒有什麼大問題,但實際上卻隱藏著危機--有些人可能是沒有 Middle Name 的,這個時候 getMiddleNameFromDB() 一般來說最合理的做法是返回 null,於是當我們的 getCapitalizeName() 被執行時,就會發生 NullPointerException。

換句話說,其實問題在於我們以前在 Java 程式中用 null 來代表沒有返回值的情況,很容易造成語意的不清,而沒辦法準確標示出某個東西是否是「可能有、有可能沒有」的。

而 Scala 中的 Option[T] 就是為了要解決這個問題,同時這個概念也被引入了 Java 8 中,所以 Java 8 也有 Option 這樣的類別,雖然細節上有所不同,但其要解決的問題都是相同的。

有了 Option[T] 這個資料型別,上述的程式碼在 Scala 中一般會被寫成:

def getFirstNameFromDB: String = "Joe"
def getMiddleNameFromDB: Option[String] = Some("T.")
def getLastNameFromDB: String = "Wood"

def capitalize(line: String) = line.charAt(0).toUpper + line.substring(1)
def getCapitalizeName() = {
    val capitalizedMiddleName = if (getMiddleNameFromDB.isDefined) { capitalize(getMiddleNameFromDB.get) + " " } else ""
    capitalize(getFirstNameFromDB) + " " +
    capitalizedMiddleName +
    capitalize(getLastNameFromDB)
}

在這裡我們可以先忽略掉 getCapitalizeName() 的實作細節,只看 getXXXNameFromDB 系列的三個函式,會發現這次我們的函式返回值總共有兩種型態--String 與 Option[String]。而由於 getMiddleNameFromDB() 返回的是 Option[String],我們可以很清楚的知道這個函式的返回值很有可能是空的--這個人沒有 Middle Name。

在這裡讀者可能會有疑惑--可是 getFirstNameFromDB 也有可能返回 null 不是嗎?沒錯,由於 Scala 為了與 Java 維持一定的交互操作性,所以 Scala 當中還是有 null 的存在,寫程式的時候,我們還是可以讓一個標註返回型別是 String 函式還是可以回傳 null 給呼叫者,不過在 Scala 的慣例裡並不會這麼做,習慣上寫 Scala 程式碼的時候不應該出現 null,函式也永遠不應該返回 null,任何原本我們習慣用 null 代表的地方都應該改用 Option[T] 來表示。

Option[T] 到底是什麼?

講了這麼多,那麼 Option[T] 到底是什麼東西呢?

我們可以這樣想--Option[T] 是一個盒子,這個盒子可能會有兩種狀況--裡面裝了一個 T 型別的東西,也有可能這個盒子是空的,裡面什麼也沒有。

換句話說,Option[Int] 就是一個裡面可能裝有一個整數的盒子,Option[String] 是一個裡面可能裝有字串的盒子,Option[java.util.Date] 是一個裡面可能裝有 Date 物件的盒子。

當我們拿到一個 Option[Int] 的時候,我們知道他是一個可以裝 Int 的盒子,但因為他還沒有打開,所以我們不曉得裡面到底有沒有東西,但我們知道他只會有兩種狀況--裡面有裝了一個 Int,又或者裡面什麼都沒有。

當我們要表示裡面裝了一個 Int 的時候,可以用 Some[Int] 來表示,而當裡面什麼都沒有的時候,我們可以用 None 這個 Singleton 物件來表示:

val boxWithInt1: Option[Int] = Some[Int](13)  // 盒子裡面裝了一個 13 的 Int
val boxWithInt3: Option[Int] = Some(12)       // 因為 Scala 有型別推論,所以 Some(12) 和 Some[Int](12) 是一樣的
val emptyIntBox: Option[Int] = None           // 空的盒子

val boxWithString: Option[String] = Some("I'm a string box")
val emptyStringBox: Option[String] = None     // 空盒子一律都是 None 這個 singleton object

val sameEmptyBox = (emptyIntBox == emptyStringBox) // true,在 Scala 的世界中,所有空盒子都是一樣的

所以有了 Option[T] 這樣的表示方法,我們就能夠很清楚地表示函式的返回值是否有可能是無意義的--如果一個人有 Middle Name,那我們就回傳給呼叫者一個裝了 Middle Name 的盒子,而如果一個人沒有 Middle Name,我們就可以回傳一個空拿子給呼叫者。

最簡單的 Option[T] 使用方式

有了上述的概念之後,我們就可以來看看要如何使用 Option[T] 這樣的資料型別。

首先要注意的是,因為 Scala 是靜態並且強型別的語言,所以 Option[Int] 和 Int 會是兩種完全不同的東西,當一個函式要求的是 Int 的時候,我們是不能傳入 Option[Int] 給他的--例如一個 Int 能夠乘上另一個整數,但不能乘上一個 Option[Int],畢竟如果這個盒子是空的要怎麼辦呢?所以這樣的行為在 Scala 裡是會產生型別錯誤的,就算我們的盒子裡確實有一個整數也是一樣。

scala> val boxWithInt: Option[Int] = Some(2)
boxWithInt: Option[Int] = Some(2)

scala> 5 * boxWithInt
<console>:9: error: overloaded method value * with alternatives:
  (x: Double)Double <and>
  (x: Float)Float <and>
  (x: Long)Long <and>
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (Option[Int])
              5 * boxWithInt
                ^

因為若我們要針對盒子裡面的東西進行操作,就先要把東西從盒子裡拿(get)出來,而當我們拿出的東西就會是 Option[T] 當中的 T 型別,例如 Option[Int] 就會拿出一個 Int,而 Option[String] 就會拿出 String。

scala> val boxWithInt: Option[Int] = Some(2)
boxWithInt: Option[Int] = Some(2)

scala> val boxWithString: Option[String] = Some("Hello World")
boxWithString: Option[String] = Some(Hello World)

scala> boxWith
boxWithInt   boxWithString

scala> boxWithInt.get * 12
res4: Int = 24

scala> boxWithString.get.toUpperCase
res5: String = HELLO WORLD

不過這裡要注意的,是如果我們對一個空的盒子呼叫 .get 的話,因為盒子根本是空的,沒有辦法從裡面拿出任何有意義的東西,這個時候就會丟出 NoSuchElement 的 Exception:

scala> val emptyBox: Option[Int] = None
emptyBox: Option[Int] = None

scala> emptyBox.get
java.util.NoSuchElementException: None.get
  at scala.None$.get(Option.scala:322)
  at scala.None$.get(Option.scala:320)
  ... 32 elided

也因為這個原因,所以實際上在使用 get 這個函式之前,我們必須先檢查要操作的 Option[T] 到底是不是空盒子,如果是的話,就要另外處理,以免產生 Exception。在這個情境下,我們可以用 isDefined 這個函式來檢查,若 Option[T] 裡面有東西,他會回傳 true,如果是空盒子的話,則會回傳 false。

def printOptionContent(box: Option[Int]) {
  if (box.isDefined) {
    println("盒子裡放的是:" + box.get)
  } else {
    println("盒子裡是空的")
  } 
}

printOptionContent(Some(12))  // 盒子裡放的是:12
printOptionContent(None)      // 盒子裡是空的

不過像這樣的做法其實和我們在 Java 當中針對可能是 null 的變數用 if / else 區塊來檢查並沒有什麼不同,唯一的差別是當我們看到 Option[T] 的時候,就可以知道這有可能是個空盒子,貌然使用 get 來拿出裡面的東西是很危險(可能會產生 Exception)的,所以使用之前一定要先檢查。

讓 Option[T] 有預設值

不過如果 Option[T] 只能提醒我們盒子裡可能是空的,而我們每次都要進行檢查的話,顯然相當地不方便,所以 Scala 的 Option[T] 也提供了一些相當好用的函式,來減化我們的工作。

其中最簡單的一個函式就是 getOrElse,透過這個函式,我們可以在取出 Option[T] 的內容的時候指定預設值--如果盒子裡有東西,就拿出盒子裡的東西,如果沒有的話,就使用預設的值。

val boxWithInt: Option[Int] = Some(12)
val emptyBox: Option[Int] = None

boxWithInt.getOrElse(0) * 3 // 因為 boxWithInt 不是空盒子,所以是 12 * 3 = 36
emptyBox.getOrElse(0) * 3   // 因為是空盒子,所以是 0 * 3 = 0

val emptyStringBox: Option[String] = None
emptyStringBox.getOrElse("Empty").charAt(0) // 因為 emptyStringBox 是空的,所以取出的是 "Empty",而他的第一個字元是 'E'

有了這個方法,當我們需要在盒子是空的時候使用預設值時,就不需要特地使用 if / else 區塊來操作,而是可以使用更簡單易懂的方式來表達「如果盒子裡沒有東西就使用預設值」這件事。

使用另一個 Option[T] 當做備胎

Option[T] 這樣的資料結構,除了可以幫助我們讓 API 更清楚,並且強迫使用者檢查可能沒有值的情況,他另一個方便的地方,是可以透過一些簡單的操作,將數個 Option[T] 的資料給組合起來。

舉例而言,當我們在撰寫手機應用程式時,若要顯示使用者的個人頭像,可能會考量到網路連線的問題,而有以下的情境出現:

  • 當手機可以上網時,優先使用網路上的頭像
  • 若無法從網路上取得頭像,則試著使用本機上之前存放的快取頭像
  • 若上述兩者都失敗,則使用預設的頭像

以這個例子而言,前面兩個都是可能會沒辦法正確取出值的操作,因為有可能使用者沒有開啟網路連線,也很有可能使用者還從未使用過我們的應用程式,因此程式中並沒有快取起來的圖檔。

但另一方面,這都不會成為讓應用程式無法繼續進行的錯誤狀態,如果我們使用了預設的圖檔,那麼只是介面上沒那麼美觀,但程式還是可以繼續運行的。

像這種情況下,就很適合用 Option[T] 這樣的資料結構來組成我們要的效果,在下面的程式碼中,為了簡化起見,我們使用 String 來代表圖像的資料的 URL 或檔案位置:

def avatarFromNetwork: Option[String] = None
def avatarFromCache: Option[String] = Some("avatarCache1234.jpg")
def avatar = avatarFromNetwork orElse avatarFromCache getOrElse "avatarDefault.jpg"

在上述的程式碼中,我們使用 avatarFromNetwork orElse avatarFromCache 這樣的語法,若讀者還記得的話,這是因為在 Scala 中若一個 method 只有一個參數,我們可以使用 operator 的方式來呼叫他。

而 orElse 這個 method 的功能如下--如果 avatarFromNetwork 已經是 Some,那就回傳 avatarFromNetwork 本身,如果 avatarFromNetwork 是 None,那就回傳 avatarFromCache,換句我說,我們可以指定另一個 Option[T] 做為我們的備胎來使用。

和會回傳 null 的函式介接

我們之前有提到過,在 Scala 中函式的返回值習慣上不會是 null,但另一方面,我們也常常會用到 Java 的函式庫,這個時候我們要如何避開 NullPointerException 的危險呢?

當然其中一個最直覺的做法是當呼叫 Java 函式的時候,針對其返回值來檢查是否為 null 後再處理,但如果我們已經有 Option[T] 這種通用的介面,為什麼不直接把 Java 函式回傳的東西轉為 Option[T] 來用呢?

要達成這樣的目的很簡單,我們直要將 Java 函式回傳的值包入 Option() 這個函式中就可以了,這個函式會檢查他的參數是不是 null,如果是的話,那就把他轉為代表空箱子的 None 物件,否則的話就會轉為 Some[T]。

舉例而言,我們知道 java.util.HashMap 這個類別的 get 函式,在找不到對應 key 的 value 值的時候,是會回傳 null 的,這個時候我們就可以用 Option() 把他包起來,讓他轉成 Option[T]。

val zipCode = new java.util.HashMap[Int, String]
zipCode.put(221, "汐止")

Option(zipCode.get(221)) // Some("汐止")
Option(zipCode.get(115)) // None

透過這個方法,我們就可以安全地使用 Java 可能會回傳 null 的函式了。

小結

這次我們看到了 Option[T] 如何替我們解決 NullPointerException 的問題,但我們也發現如果單純使用 Option[T] 提供的函式來操作,那麼有的時候程式碼似乎反而不太容易理解。

但是先別著急,Scala 提供了 Pattern Matching 和 for-comprehension 這兩個相當強大的控制結構來解決這個問題,而從下次開始,我們就會針對這兩個相當重要的控制結構進行介紹。

後續 >> Scala Tutorial(10)不只可以列舉值的 Pattern Matching

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

熱門論壇文章

熱門技術文章