1. 程式人生 > >Kotlin Type? vs Scala Option

Kotlin Type? vs Scala Option

本文由 KnewHow 發表在 ScalaCool 團隊部落格。

最近閱讀一些關於 Kotlin 型別系統方面的書,發現 Kotlin 的型別系統針對 null 有著獨特的設計哲學。在 Java 或者其它程式語言中,經常會出現 NullPointerException,而導致此異常的重要原因是因為你可以寫 String s = null 這樣的程式碼。其實可以認為這是 Java 等語言型別系統設計的一個缺陷,它們允許 null 可以作為任何型別的值!

但是在 Kotlin 中,如果你宣告 val s: String = null,那麼編譯器會給你一個 error,因為在 Kotlin 中,你不允許把一個 null

值賦給一個普通的型別。如果你宣告一個這樣的函式 fun strLen(s: String) = {...},那麼這個函式將不接受值為 null 的引數。

這個設計看起來如此的美好,他可以極大程度的減少 Kotlin 產生 NullPointerException,可是如果有一天,你需要呼叫一個方法,它的返回值可能為 null 也可能為 String ,那麼在 Kotlin 中你可以宣告一個可空的字串型別:String?val s: String? = null 此時 Kotlin 的編譯器會讓這行程式碼通過。當然它也可以接收一個普通的 String 型別的值 val s: String? = "abc"

可空型別(Type?)的設計,是 Kotlin 另一個設計哲學,它要求工程師在設計的時候就需要確定該變數是否可為空。如果不為空就使用Type 型別宣告,否則就使用 Type? 型別宣告。這讓我想起在 Scala 中存在一個和 Type? 有著異曲同工之妙的一個型別—— Option[T]

Option[T] 有兩個子型別:Some[T]None,你可以使用 val s: Option[String] = Some("123") 來表示一個字串存在,當然你可以使用val s: Option[String] = None 來表示這個字串不存在。

Scala 和 Kotlin 都是基於 JVM 的程式語言,而 Option[T]

Type? 的設計就是用來解決 JVM 平臺出現的 NullPointerException。但二者的設計理念卻截然不同,Scala 的 Option[T] 是在原有型別基礎上使用 Option 做一層封裝,而 Koltin 的 Type? 是使用語法糖完成的。

那麼這兩種設計方案到底誰更好一點呢?我們將會使用以下標準來分別測試它們:

  • 是否可以完美的規避 NullPointerException —— 二者的設計都是為了解決 NullPointerException,誰可以更好的規避這個問題呢?
  • 程式碼的可讀性 —— 如果在複雜的業務中,誰的程式碼可讀性更好一點呢?
  • 效能

規避空指標

在上文中,我們曾經提過,NullPointerException 產生的原因是你可以把一個 null 的值傳遞給一個型別的變數,然後呼叫這個型別的方法。我們可以使用 Java 的程式碼來表示一下:String s = null; s.length()

Type? 的設計理念中,對於不確定是否為 null 型別可以使用 Type? 型別來宣告,如val s: String? = getString...,此時 s 的型別是 String?,你不能直接呼叫 s.length,你需要進行安全呼叫s?.length。這個函式的返回型別是一個 Int?,這很正常,對於一個不確定是否為 null 的型別進行安全呼叫返回當然是一個 Type? 型別。如果 s 不為 null 正常返回 s 的長度,否則返回 null。除此之外, Kotlin 還針對 Type? 提供了 Elvis 操作和 let 函式,具體的用法可以參考 Kotlin 官方手冊。

而在 Optional 的設計哲學中,你可以使用 Option[T] 來包裹一個不確定是否為 null 的值。這裡我們使用 Scala 的程式碼來演示:val s: Option[String] = Option(getString...),此時 s 的型別為 Option[String],你仍然不能直接呼叫s.length,你可以使用 map 函式:s.map(s => s.length),它的返回值是一個 Option[Int] 型別。和 Type? 很類似,對一個 Option[T] 型別使用 map 函式,結果當然是一個 Option[S] 型別。在 Scala 中,你也可以使用模式匹配來處理 Option 型別。

總結:二者都可以完美的規避 NullPointerExceptionType? 使用安全呼叫來避免直接呼叫 Type 型別的方法,而 Option 則使用 map 函式或者模式匹配來處理。本質上都是避免直接呼叫值可能為 null 的型別變數的方法。

程式碼可讀性

實際的業務是比較複雜的,例如,我們需要計算兩個數字字串的乘積,首先我們需要把他們轉換為 Int 型別,如果其中一個字串是轉換失敗,則無法計算結果。

在 Kotlin 的 Type? 中,我們需要重新定義 String 型別的 toInt 方法,讓它返回一個 Int? 型別,程式碼如下:

fun tryString2Int(a: String) = try {
    a.toInt()
}catch (e:Exception){
    null
}
複製程式碼

然後我們需要定義一個方法來計算兩個數字字串的乘積,這裡我們使用 Type? 的 let 函式,它接受一個 Lambda 表示式,如果呼叫者的值不為 null,則呼叫 Lambda 表示式,否則直接返回 nullstrNumberMuti 函式返回的是一個 Double? 型別,如果有任何一個字串轉換數字失敗,就返回 null,都轉換成功才計算乘積。

fun strNumberMuti(s1: String, s2: String): Double? =
    tryString2Int(s1)?.let{ a ->
        tryString2Int(s2)?.let {
            t -> a * t * 1.0 }}
複製程式碼

這段程式碼的可讀取有點差呀,而且在實際的業務開發過程中,可能會有更多的 Type? 型別,那程式碼豈不是要爆炸了!。幸運的是,Kotlin 允許我們使用 if 來代替 let 函式 做相同的判斷,程式碼如下:

fun strNumberMuti2(s1: String, s2: String):Double? {
    val a = tryString2Int(s1)
    val b = tryString2Int(s2)
    return if(a!=null && b!= null) a * b * 1.0 else null
}
複製程式碼

這樣的程式碼可讀性就好多了,但是丟失函式式的程式設計美感。而且感覺 Type? 是一種語法糖,手動對 Type? 進行非空校驗,就可以直接使用 Type 型別了!!

同樣的我們使用 Scala 的 Option[T] 來完成上面的需求,為了讓 toInt 函式返回 Option[T] 型別,我們定義了一個 Try 函式,這個函式看不懂沒關係,你只需知道它接受一個函式,並且返回一個 Option[A] 值即可。程式碼讓如下:

def Try[A](a: => A): Option[A] = {
    try Some(a)
    catch {case e: Exception => None}
  }
複製程式碼

同樣的,我們需要寫一個函式,用來把兩個字串數字轉換為整數,並且做它們的乘積,這裡我們為了使程式碼更簡潔,使用了 Scala 的 for 推導,具體的用法可以參考 Scala 官方的 Document。strNumberNu返回型別是 Option[Double],如果有任何一個轉換失敗,返回 None,否則返回 Some[Double],程式碼如下:

 def strNumberMuti(s1: String, s2: String): Option[Double] = {
    for{
      a <- Try{ s1.toInt }
      b <- Try{ s2.toInt }
    } yield a * b

  }
複製程式碼

可以看出,使用 Scala 的 Option[T] 更具有函式式的程式設計美感,而且程式碼的可讀性極強,而且即使有更多的 Option[T],for 推導都可以輕鬆應對。

總結:面對比較複雜的業務場景,Type?Option[T] 都可以輕鬆應對,但是 Type? 的用法就顯得有些 low,還是使用 !=null 的套路,這也暴露了它的設計是存在缺陷的。相反的 Option[T] 的設計理念是完備的,而且極具函式式的程式設計美感。

效能

效能是衡量設計好壞的一個重要的方面,下面我們只做一個簡單的測試:讓兩個字串都是"999",然後分別執行 Kotlin 的 strNumberMuti 和 Scala 的 strNumberMuti 一千萬次,然後我們發現 Kotlin 的 strNumberMuti 執行時間大約在 1.9s,而 Scala 的 strNumberMuti 執行時間約在 5.0s。由此可以看出,Kotlin 的 Type? 比 Scala Option[T] 擁有更好的效能,其實這樣很正常,因為 Kotlin 的 Type? 是語法糖,建立一個 Type? 的物件其實和建立一個 Type 的物件其實消耗的效能差不多,但是 Option[T]不僅僅需要建立 T 型別的物件,更需要建立 Option[T] 型別的物件來包裹 T 型別的物件,因此它的開銷大一點。

總結

就我而言,我更喜歡 Scala 的 Option[T] 的設計,因為它是理論完備的,而且極具函式式的程式設計美感,即使它的效能要差一點。對於 Kotlin 的 Type? 型別,我覺得它的設計有瑕疵,就拿 let 函式舉例,在單個 Type? 很好用,但是當多個 Type? 進行組合的時候,就顯得很雞肋。

蘿蔔青菜,各有所愛,也許某天 Kotlin 也會讓 Type? 具有函式式的程式設計美感。