#Kotlin# 一年の使用報告 - 類型設計

前言

大約有一年的 Kotlin 使用歷史,總結下 Kotlin 帶來的一些體驗。這是該系列文章的第一篇。

正文

昨天,著名 PL 人王垠寫了篇文章 Java 有 value type 嗎?是的十分有意思,文中假設將 Java 中的原子類型設計為 reference type,你會發現它依然和 value type 表現一致。一個很重要的原因是 Java 中並不具備 C 語言中的 deref 操作符,無法修改 reference 指向中真實的 value,你在對它進行再賦值時,僅僅是改變了它的指向。

然而實際的情況更複雜一點。拿 int 類型舉例,在 Java 中除了 int 還提供對其 wrap 過的 Integer 類型。Integer 明顯是引用類型但是和值類型表現一致,因為它並不提供 setValue() 這樣改變內值的函數。可以看出 Java 是在刻意保持 Integer 和 int 表現的一致性(自動裝箱也是一方面)。

好吧,至少在 Kotlin 中你的顧慮可以少了一點,因為在 Kotlin 中只有 Int 而沒有 int。它可以像 Integer 一樣提供一系列額外的操作函數(可以看下面的代碼例子),然而實際上在 JVM 上卻儲存的是 int 類型!(int 效率會更高)也就是說 Kotlin 中 Int 類型編譯成位元組碼後實際上是 int 類型,它利用編譯器隱藏了一些實現,把 int 等原子類型看起來更像 reference type,並提供了額外的函數:

10.ushr(10)

看著是不是很屌。這樣看來,Kotlin 中也就實現了語法上一切皆 reference 的設計。

順便講一個 trick 吧。你思考下 Kotlin 中的 Int 和 Int?(也就是 nullable 的 Int) 類型。有沒有發現什麼不對勁的地方。是的,如果如上文所說 Kotlin 中的 Int 類型實際上是用 value type 的 int 實現的話,怎麼可能會存在 nullable 的情況(只有 reference type 才有 null 這個概念)。好吧告訴你!Kotlin 中 Int? 的實現居然變成了 Java 中的 Integer 了。。我次,是不是有種被騙的感覺。

然而仔細想想,Int? 的出現不就更符合一切皆 reference 的設計的(都能設置為 null 了,它當然是 reference type 啊 =_=)。可以看出這一系列的暗箱操作都是為了實現基本類型的歸一化處理呀。

Kotlin 中使用 var 和 val 來界定 reference 的可變性,這其實是對 final 修飾符的泛化。帶來的結果是,它讓我更關注 reference 應該是 mutable 還是 immutable 的。我踐行的一種思想是 [ 盡量保持 reference 是 immutable 的 ] ,一個適合的場景:

data class Test(val a: Int, val b: String)

我使用 val 來標記 Test 類中的所有成員變數。它在構造後就無法對 a、b 成員進行改寫操作。

此外,Kotlin 為解決 NullPointerException 做了很多類型安全的工作,這在之前的文章中就有提及。對我的影響是有好有壞,它強制我更正確對待所有可能為空的引用,但是很多地方我為了避免充斥 check null 運算,我反而更多地使用了 lateinit 關鍵字來描述變數為 notnull。例如:

lateinit var textView: TextViewfun onCreate() { textView = find(R.id.textView)}

然而這並不是一種好的習慣。除非你十分確保 textView = find(R.id.textView) 這句語句執行時 textView 尚未被賦值,否則將會拋出錯誤拒絕對 textView 進行二次賦值。特別是在 Activity 或者 Fragment 這種生命周期由系統託管的類中你尤其需要小心。例如上面的代碼看上去很正確,但是假設是它在 Retained Fragment 中進行的話就會發生意外(在屏幕發生旋轉後程序就會崩潰退出)。原因在於 textView 的實例並未被釋放,於是在第二次調用 onCreate() 生命周期時產生了二次賦值。

延伸一下,lateinit 修飾符的原理究竟是怎樣的?查看下位元組碼的實現的話,你會發現上面代碼的等價 Java 實現為:

public TextView textView = null;void onCreate() { if (textView != null) { // 拋出錯誤 } else { textView = (TextView)findViewById(R.id.textView); }}

好吧,知道為什麼 lateinit 不能修飾 Int、Long 等基礎類型了么?因為 Int、Long 在 JVM 中的實現是 int、long 啊,上面已經說了它們是不能賦值為 null 的。

那 Lazy Delegate 呢(by lazy { //... })?這和 lateinit 完全是不同的東西,Delegate 的實現顯然也更複雜些。它們的區別在於 lateinit 是語法級別的,而 Lazy Delegate 不是,lateinit 是主動賦值,而 Lazy Delegate 是被動賦值(第一次被訪問時才賦值)。

要注意的 Lazy Delegate 和 lateinit 一樣存在我上面說的生命周期問題。Lazy Delegate 並不會報錯,但是會無法更新內值。JakeWharton 的 kotterknife 就是基於 Lazy Delegate 進行 bindView 的,所以我在新版本的 kotgo 框架中已經移除了所有 kotterknife 相關的代碼,它們存在一定的隱患。(後話就是我使用了 kotlin 更為強大的 synthetic 來綁定試圖了~)

前面說了一堆,但是Kotlin 的類型設計中,我認為最大亮點的依然是 function type,作為 first class type,你甚至可以把函數當成值進行傳遞。這直接賦予了 Kotlin 函數式編程的特性,實在太 cool 了:

fun a() = 1val b = ::afun c(f: ()->Int) = f()val d = c(b)

而 lambda 函數的使用場景就更加多了。在我們的最近的產品中,因為大量地使用到了基於函數式編程思想的 ReactiveX,於是 lambda 成為了這裡的救星:

GankService.api.getMeizi() .subscribeOn(Schedulers.io()) .map { Hawk.put("meizis", it.results) it.results } .onErrorResumeNext { val meiziList: List<Meizi> = Hawk.get("meizis") ?: throw GankServiceException(it.message) Observable.just(meiziList) }

看起來實在是太優美了。

噢對,Jack Tools 目前也能做到。話說 Jack Tools 的誕生應該算的上是 Android 開發工具上的一個里程碑了,它對多年被困在 Java 6/7 中的 Android 開發者來說簡直是個救命稻草。但只要開發者還是在 Java 這棵樹上就依然沒有根本性地解放生產力,編程語言界一些更好玩、更先進的思想都不知道他們啥時候才能玩得上呀~(霧)

推薦閱讀:

Kotlin 終於成為了 Android 的官方支持語言
#Kotlin# Activity 之朝花夕拾
Kotlin 資源大全 - 如何學習 Kotlin?

TAG:Android开发 | Kotlin |