本文由 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
型別。
總結:二者都可以完美的規避 NullPointerException
,Type?
使用安全呼叫來避免直接呼叫 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 表示式,否則直接返回 null
。strNumberMuti
函式返回的是一個 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?
具有函式式的程式設計美感。