Kotlin 1.4 新特性預覽

Kotlin 1.4 沒有特別重大的更新,更多的是細節的優化。html

1. 安裝 Kotlin 1.4

Kotlin 1.4 的第一個里程碑版本發佈了,具體發佈信息能夠在這裏查看git

生產環境當中最好仍然使用 Kotlin 的穩定版本(例如最新的 1.3.71),若是你想要馬上立刻體驗 1.4 的新特性,那麼個人建議是先安裝一個 EAP 版本的 IntelliJ IDEA EAP 版本是 IntelliJ IDEA 2020.1 Beta,而後再在這個版本的 IntelliJ 上安裝最新版的 Kotlin 插件,這樣既能夠繼續使用 1.3 作項目,又不耽誤體驗新特性:github

圖 1:IntelliJ IDEA EAP 版本與正式版能夠共存

安裝 Kotlin 1.4 的插件方法想必你們都已經輕車熟路了,打開設置,搜 Kotlin,找到插件版本管理的下拉菜單,選擇 Early Access Preview 1.4.x 便可:面試

圖 2:升級 Kotlin 插件

圖 2:升級 Kotlin 插件

好了,重啓 IntelliJ,新建一個工程試試看吧~~算法

2. 主要的語法更新

接下來咱們就按照官方博客給出的介紹 Kotlin 1.4-M1 Released 來體驗下新特性。數組

本文源碼均已整理至 GitHub:Kotlin1.4FeaturesSamplemarkdown

2.1 Kotlin 接口和函數的 SAM 轉換

一個就是你們期待已久的 Kotlin 接口和函數的 SAM 轉換。得益於新的類型推導算法,以前一直只有調用接收 Java 單一方法接口的 Java 的方法時才能夠有 SAM 轉換,如今這個問題不存在了,且看例子:併發

//注意 fun interface 是新特性
    fun interface Action {
        fun run()
    }
    
    // Kotlin 函數,參數爲 Kotlin 單一方法接口
    fun runAction(a: Action) = a.run()
    // Kotlin 函數,參數爲 Java 單一方法接口
    fun runRunnable(r: Runnable) = r.run()
複製代碼

在 1.4 之前,咱們只能:app

runAction(object: Action{
        override fun run() {
            println("Not good..")
        }
    })
複製代碼

或者ide

runAction(Action { println("Not good..") })
複製代碼

runRunnable 函數雖然接收的是 Java 的接口,一樣不支持 SAM。

如今在 1.4 當中呢?

runAction { println("Hello, Kotlin 1.4!") }
    runRunnable { println("Hello, Kotlin 1.4!") }
複製代碼

真是妙啊。

2.2 類型推導支持了更多的場景

類型推導讓 Kotlin 的語法得到了極大的簡潔性。不過,你們在使用 Kotlin 開發時,必定會發現有些狀況下明明類型是很肯定的,編譯器卻必定要讓咱們顯式的聲明出來,這其實就是類型推導算法沒有覆蓋到的場景了。

例如如下代碼在 Kotlin 1.3 當中會提示類型不匹配的問題:

val rulesMap: Map<String, (String?) -> Boolean> = mapOf(
        "weak" to { it != null },
        "medium" to { !it.isNullOrBlank() },
        "strong" to { it != null && "^[a-zA-Z0-9]+$".toRegex().matches(it) }
    )
複製代碼

圖 3:Kotlin 1.3 中提示類型不匹配

博客原文中給出的這個例子乍一看挺複雜,仔細想一想問題主要在於咱們能夠經過 rulesMap 的類型來肯定 mapOf 的返回值類型,進而再肯定出 mapOf 的參數類型,即 Pair 的泛型參數類型。類型信息是充分的,不過這段代碼在 Kotlin 1.4 之前是沒法經過編譯的,應該是類型推導的層次有點兒多致使算法沒有覆蓋到。好在新的推導算法解決了這個問題,可以應付更加複雜的推導場景。

2.3 Lambda 表達式最後一行的智能類型轉換

這個比較容易理解,直接看例子:

val result = run {
       var str = currentValue()
        if (str == null) {
            str = "test"
        }
        str // the Kotlin compiler knows that str is not null here
    }
    // The type of 'result' is String? in Kotlin 1.3 and String in Kotlin 1.4```
複製代碼

這裏 result 做爲 run 的返回值,實際上也是 run 的參數 Lambda 的返回值,所以它的類型須要經過 str 的類型來推斷。

在 1.3 當中,str 的類型是能夠推斷成 String 的,由於 str 是個局部變量,對它的修改是可控的。問題在於雖然 str 被推斷爲 String 類型,Lambda 表達式的返回值類型卻沒有使用推斷的類型 String 來判斷,而是選擇使用了 str 的聲明類型 String?。

在 1.4 解決了這個問題,既然 str 能夠被推斷爲 String,那麼 Lambda 表達式的結果天然就是 String 了。

稍微提一下,IntelliJ 的類型提示貌似有 bug,有些狀況下會出現不一致的狀況:

圖 4:疑似 IntelliJ 行內的類型提示的 bug

咱們能夠經過快捷鍵查看 result 的類型爲 String,可是行內的類型提示卻爲 String?,不過這個不影響程序的運行。

固然,有些開發者常常會抱怨相似下面的這種狀況:

var x: String? = null
    
    fun main() {
        x = "Hello"
        if(x != null){
            println(x.length) 
        }
    }
複製代碼

我明明已經判斷了 x 不爲空,爲何卻不能自動推導成 String?請必定要注意,這種狀況不是類型推導算法的問題,而是 x 的類型確實沒法推導,由於對於一個共享的可變變量來說,任何前一秒的判斷都沒法做爲後一秒的依據。

2.4 帶有默認參數的函數的類型支持

若是一個函數有默認參數,咱們在調用它的時候就能夠不傳入這個參數了,例如:

fun foo(i: Int = 0): String = "$i!"
複製代碼

調用的時候既能夠是 foo() 也能夠是 foo(5),看上去就如同兩個函數同樣。在 1.4 之前,若是咱們想要獲取它的引用,就只能獲取到 (Int) -> String 這樣的類型,顯得不是很方便,如今這個問題解決了:

fun apply1(func: () -> String): String = func()
    fun apply2(func: (Int) -> String): String = func(42)
    
    fun main() {
        println(apply1(::foo))
        println(apply2(::foo))
    }
複製代碼

不過請注意,一般狀況下 ::foo 的類型始終爲 (Int) -> String,除了做爲參數傳遞給接收 () -> String 的狀況下編譯器會自動幫忙轉換之外,其餘狀況下是不能夠的。

2.5 屬性代理的類型推導

在推斷代理表達式的類型時,以往不會考慮屬性代理的類型,所以咱們常常須要在代理表達式中顯式的聲明泛型參數,下面的例子就是這樣:

import kotlin.properties.Delegates
    
    fun main() {
        var prop: String? by Delegates.observable(null) { p, old, new ->
            println("$old$new")
        }
        prop = "abc"
        prop = "xyz"
    }
複製代碼

這個例子在 1.4 中能夠運行,但若是是在 1.3 當中,就須要明確泛型類型:

var prop: String? by Delegates.observable<String?>(null) { p, old, new ->
        println("$old$new")
    }
複製代碼

2.6 混合位置參數和具名參數

位置參數就是按位置傳入的參數,Java 當中只有位置參數,是你們最熟悉的寫法。Kotlin 支持了具名參數,那麼入參時兩者混合使用會怎樣呢?

圖 5:1.3 當中不容許在具名參數以後添加位置參數

1.3 當中,第三個參數會提示錯誤,理由就是位置參數前面已經有了具名參數了,這是禁止的。這樣主要的目的也是但願開發者可以避免寫出混亂的入參例子,不過這個例子彷佛並不會有什麼使人疑惑的地方,因而 1.4 咱們能夠在具名參數後面跟位置參數啦。

其實這個特性並不會對入參有很大的影響。首先位置參數的位置仍然必須是對應的,其次具名參數的位置也不能亂來。例如咱們爲例子中的 a 添加一個默認值:

圖 6:1.4 當中具名參數以後添加位置參數須要保證位置對應

注意圖 6 是 1.4 環境下的情形,這樣調用時咱們就能夠沒必要顯式的傳入 a 的值了,這時候直覺告訴我參數 b 後面的參數應該是 c,然而編譯器卻不領情。這樣看來,即使是在 1.4 當中,咱們也須要確保具名參數和位置參數與形參的位置對應才能在具名參數以後添加位置參數。

所以,我我的的建議是對於參數比較多且容易混淆的情形最好都以具名參數的形式給出,對於參數個數較少的情形則能夠所有采用位置參數。在這裏還有另外的一個建議就是函數的參數不宜過多,參數越多意味着函數複雜度越高,越可能須要重構。

2.7 優化屬性代理的編譯

若是你們本身寫過屬性代理類的話,必定知道 get 和 set 兩個函數都有一個 KProperty 的參數,這個參數其實就是被代理的屬性。爲了獲取這個參數,編譯器會生成一個數組來存放這代理的屬性,例如:

class MyOtherClass {
        val lazyProp by lazy { 42 }
    }
複製代碼

編譯後生成的字節碼反編譯以後:

public final class com.bennyhuo.kotlin.MyOtherClass {
      static final kotlin.reflect.KProperty[] ?delegatedProperties;
      static {};
      public final int getLazyProp();
      public com.bennyhuo.kotlin.MyOtherClass();
    }
複製代碼

其中 ?delegatedProperties 這個數組就是咱們所說的存被代理的屬性的數組。不過,絕大多數的屬性代理其實不會用到 KProperty 對象,所以無差異的生成這個數組其實存在必定的浪費。

所以對於屬性代理類的 get 和 set 函數實現爲內聯函數的情形,編譯器能夠確切的分析出 KProperty 是否被用到,若是沒有被用到,那麼就不會生成這個 KProperty 對象。

這裏還有一個細節,若是一個類當中同時存在用到和沒用到 KProperty 對象的兩類屬性代理,那麼生成的數組在 1.4 當中只包含用到的 KProperty 對象,例如:

class MyOtherClass {
        val lazyProp by lazy { 42 }
        var myProp: String by Delegates.observable("<no name>") {
                kProperty, oldValue, newValue ->
            println("${kProperty.name}: $oldValue -> $newValue")
        }
    }
複製代碼

其中 myProp 用到了 KProperty 對象,lazyProp 沒有用到,那麼生成的 ?delegatedProperties 當中就只包含 myProp 的屬性引用了。

2.8 參數列表最後的逗號

這個需求別看小,很是有用。咱們來看一個例子:

data class Person(val name: String, val age: Int)
    
    fun main() {
        val person = Person(
            "bennyhuo",
            30
        )
    }
複製代碼

Person 類有多個參數,傳參的時候就會出現前面的參數後面都有個逗號,最後一個沒有。這樣看上去好像也沒什麼問題是吧?那有可能你沒有用到過多行編輯:

圖 7:多行編輯逗號的問題

這裏這個逗號有時候會特別礙事兒,但如何每一行均可以有一個逗號這個問題就簡單多了:

圖 8:多行編輯全部參數

除了這個場景以外,還有就是調整參數列表的時候,例如我給 Person 在最後加了個 id,我還得單獨給 age 的參數後面加個逗號:

圖 9:增長參數給原來的參數加逗號

這時候我又以爲 id 應該放到最前面,因而作了個複製粘貼,發現仍是要修改逗號。固然,最後的這個功能 IntelliJ 有個快捷鍵能夠直接交換行,同時幫咱們自動處理逗號的問題,不過總體上這個小功能仍是頗有意思的。

提及來,JavaScript 當中的對象字面量當中也容許最後一個字段後面加逗號:

圖 10:JavaScript 的對象字面量

不過請注意,儘管它與 JSON 有着深厚的淵源,但 JSON 的最後一個字段後面是不容許加逗號的(固然還有字段要加引號)。

2.9 when 表達式中使用 continue 和 break

continue 和 break 的含義沒有任何變化,這兩者仍然在循環當中使用,只不過循環內部的 when 表達式當中在以前是不可使用 continue 和 break 的,按照官方的說法,他們以前有意將 continue 或者 break 用做 when 表達式條件 fallthrough 的,不過看樣子如今還沒想好,只是不想再耽誤 continue 和 break 的正常功能了。

2.10 尾遞歸函數的優化

尾遞歸函數估計你們用的很少,這裏主要有兩個優化點

  • 尾遞歸函數的默認參數的初始化順序改成從左向右:

  • 尾遞歸函數不能聲明爲 open 的,即不能被子類覆寫,由於尾遞歸函數的形式有明確的要求,即函數的最後一個操做必須只能是調用本身,父類的函數聲明爲 tailrec 並不能保證子類可以正確地按要求覆寫,因而產生矛盾。

圖 11:1.4 中尾遞歸函數的默認參數列表初始化順序

2.11 契約的支持

從 1.3 開始,Kotlin 引入了一個實驗特性契約(Contract),主要來應對一些「顯而易見」狀況下的類型推導或者智能類型轉換。

在 1.4 當中,這個特性仍然會繼續保持實驗狀態,不過有兩項改進:

  • 支持使用內聯特化的函數來實現契約
  • 1.3當中不能爲成員函數添加契約,從1.4開始支持爲 final 的成員函數添加契約(固然任意成員函數可能存在被覆寫的問題,於是不能添加)

2.12 其餘的一些改動

除了語法上的明顯的改動以外,1.4 當中也直接移除了 1.1-1.2 當中協程的實驗階段的 API,有條件的狀況下應該儘快去除對廢棄的協程 API 的使用,若是暫時沒法完成遷移,也可使用協程的兼容包 kotlin-coroutines-experimental-compat.jar。

剩下的主要就是針對編譯器、使用體驗的各類優化了,實際上這纔是 Kotlin 1.4 最重要的工做。這些內容相對抽象,我就不作介紹了。

補充一點,在本文撰寫過程當中,我使用 IntelliJ IDEA 2019.3.3 來運行 Kotlin 1.3,使用 IntelliJ IDEA 2020.1 BETA 來運行 Kotlin 1.4-M1,結果發現後者的代碼提示速度彷佛有明顯的提高,不知道是否是個人錯覺,你們能夠自行感覺下並發表你的評論。

3. 小結

Kotlin 目前的語法已經比較成熟了,仍是那句話,提高開發體驗,擴展應用場景纔是它如今最應該發力的點。

將來可期。

若是你們想要快速上手 Kotlin 或者想要全面深刻地學習 Kotlin 的相關知識,能夠關注我基於 Kotlin 1.3.50 全新制做的新課,課程初版曾幫助3000多名同窗掌握 Kotlin,此次更新迴歸內容更精彩:

掃描二維碼或者點擊連接《Kotlin 入門到精通》便可進入課程啦!

想要找到好 Offer、想要實現技術進階的迷茫中的 Android 工程師們,推薦你們關注下個人新課《破解Android高級面試》,這門課涉及內容均非淺嘗輒止,目前已經有700+同窗在學習,你還在等什麼(*≧∪≦):

掃描二維碼或者點擊連接《破解Android高級面試》便可進入課程啦!

相關文章
相關標籤/搜索