Kotlin 1.4-M1正式釋出

kotliner發表於2020-03-31

我們很高興迎來了重要版本的第一個預覽版: Kotlin 1.4-M1.

幾個月前,我們釋出了展望Kotlin 1.4的部落格。在臨近正式釋出之時,我們將提供預覽版,讓你能親自嘗試部分新功能。

m1_twitter-01

在這篇博文中,我們將重點介紹1.4-M1以下新功能和關鍵改進:

  • 預設情況下,已開啟了一個新的,更強大的型別推斷演算法
  • 契約現已支援final成員函式了。
  • Kotlin/JVM編譯器現已會在Java 8+目標的位元組碼中生成型別註解。
  • 針對Kotlin/JS的新後端,並對產生的元件進行了重大改進。
  • 標準庫變動:deprecated週期結束,同時棄用額外的部分。

你可以在更新日誌檢視完整的變動列表。一如既往,我們非常感謝其他貢獻者

我們強烈建議你嘗試預覽版,我們將非常感激你在問題跟蹤器中提供的所有反饋。

更強大的型別推斷演算法

Kotlin 1.4使用了新的更強大的型別推斷演算法。在Kotlin 1.3中,你可以透過指定編譯器選項來嘗試這種演算法,1.4中已預設開啟。你可以在YouTrack中查閱新演算法所解決問題的完整列表。在這篇文章中,我們將重點介紹最明顯的改進。

SAM型別的Kotlin函式和介面

SAM意味著當介面”只有單一抽象方法”時,可以將其簡化為lambda。在以前,SAM轉換隻能在Kotlin的Java方法和Java介面上執行,現在你也可以在Kotlin函式和介面上使用了。

儘管Kotlin介面現在支援SAM轉換。但注意它的工作方式不同於Java:需要你顯式標記函式和介面。在使用fun關鍵字標記介面後,只要引數是該介面,就可以向其傳遞一個lambda了:

可以閱讀之前的博文瞭解有關SAM的更多細節。

Kotlin一開始就支援Java介面的SAM轉換,但是有一種情況不被支援,便是部分Java庫有時會很煩人。如果呼叫以兩個SAM介面作為引數的Java方法,則兩個引數都必須同為lambda或物件。不能向其中一個引數傳遞lambda,而向另一個引數傳遞物件。新演算法解決了這個問題,你可以隨意傳遞lambda而不是SAM介面,我們認為這能符合你的預期。

適配更多場景的型別推斷演算法

在更多的使用場景下,舊的推斷演算法需要你顯式宣告,而新的推斷演算法則能正確推斷型別了。例如在下面的示例中,lambda的引數it的型別被正確推斷為String ?了:

在Kotlin 1.3,你需要引入一個顯式的lambda引數,或將to替換為建構函式具有顯式泛型引數的Pair,才能正常工作。

對lambda結尾表示式的智慧轉換

在Kotlin 1.3中,除非你指定型別,否則lambda內的最後一個表示式不會執行智慧轉換。因此以下示例中,Kotlin 1.3會將result變數的型別推斷為String?

而在Kotlin 1.4,得益於新的推斷演算法,lambda中最後一個表示式也會被智慧轉換了,因此,lamda表示式的結果型別將被推斷為新的更精確的型別。則result變數的型別最終為String

在Kotlin 1.3,經常需要你指定顯式轉換(如!!as String這樣的強制轉換)以實現這樣的效果,而現在已經不需要新增這樣的強制轉換了。

可執行引用的智慧轉換

在Kotlin 1.3,你無法訪問經過智慧轉換的型別的成員函式。但現在你可以這樣:

在animal變數經過智慧轉換到指定的CatDog型別後,可以訪問不同的成員函式animal::meowanimal::woof。 在型別檢查之後,便可以訪問與子型別對應的成員函式。

更優異的可執行引用推斷

現在,傳遞帶預設引數函式的可執行引用更加方便了。例如,以下foo函式的可執行引用可以理解為需要一個Int引數或不需要任何引數:

更優異的代理屬性推斷

在解析緊跟關鍵字by的委託表示式時,不會去判斷委託屬性的型別。例如,以下程式碼在以前無法編譯,但是現在編譯器能正確地將引數old和引數new的型別推斷為String?了:

語言變化

之前的博文已介紹了大部分的語言變化了:

在這一篇文章裡,我們將著重介紹有關契約的小改進。

支援契約

雖然自定義契約語法仍處於試驗階段,但是我們已經支援了部分會用到契約的新用例。

現在你可以使用reified的泛型引數來定義契約。例如,你可以為assertIsInstance函式實現如下契約:

由於T型別引數是reified,因此可以在函式體中檢查其型別。現在在契約中也可以如此實現了。後續將向kotlin.test庫新增一個類似的資訊斷言函式。

另外,你現在可以為final成員自定義契約了。此前是禁止向成員函式定義契約的,因為在層級的部分成員上定義契約意味著相關的契約也具有層級結構,而這仍然是需要設計和討論的問題。但如果成員函式是final函式且未覆寫任何其他函式,則可以安全地為其定義契約。

標準庫的變動

清理廢棄的實驗協程

在1.3.0版中,kotlin.coroutines.experimentalAPI已被標記deprecated,它的替代為kotlin.coroutines。在1.4-M1中,我們從標準庫中刪除了kotlin.coroutines.experimental以結束其deprecated的流程。對於仍然在JVM上使用它的使用者,我們提供了一個相容包kotlin-coroutines-experimental-compat.jar及所有實驗性的協程API。我們將其釋出到了Maven,並將其包含在標準庫旁邊的Kotlin發行版中。同時與1.4-M1元件一起釋出到了Bintray倉庫。

刪除廢棄的mod運算

另一個廢棄的函式是mod運算子,該運算子可獲取除法運算的餘數。在Kotlin 1.1中,它被rem()函式取代。而現在我們將其從標準庫中完全刪除。

棄用浮點數向Byte和Short型別的轉換

標準庫包含將浮點數轉換為整數型別的函式:toInt()toShort()toByte()。由於數值範圍狹窄且變數儲存體積較小,將浮點數轉換為ShortByte可能會導致意外結果。為避免此類問題,從1.4-M1開始,我們不建議在DoubleFloat上呼叫toShort()toByte()函式。如果仍然需要將浮點數轉換為ByteShort,請執行兩次轉換:先轉換為Int,然後轉換為目標型別。

通用反射API

我們修改了通用反射API。現在,它包含了所有的三個目標平臺(JVM,JS,Native)上可用的成員,因此你可以確保同一份程式碼可在任一平臺上工作。

use()和時間測量函式的新契約

我們正在擴大標準庫中契約 的使用範圍。在1.4-M1中,我們為單次執行程式碼塊use()函式以及時間測量函式measureTimeMillis()measureNanoTime()新增了契約宣告。

用於Kotlin反射的Proguard配置

從1.4-M1開始,我們在kotlin-reflect.jar中內嵌了Kotlin Reflection的Proguard/R8配置。有了這個,就能使大多數R8或Proguard的Android專案可以在沒有其他配置的情況下使用kotlin-reflect進行工作了。不再需要你去複製貼上Kotlin反射包內部的Proguard配置。但是請注意,你仍然需要明確羅列所有用到反射的API。

Kotlin/JVM

從1.3.70版開始,Kotlin能夠在JVM位元組碼(目標版本1.8+)中生成型別註解,以便在執行時能獲取到它們。社群要求這個特性有一段時間了,因為能讓使用部分現有Java庫變得更加容易,併為新庫的作者提供了更多功能。

在以下示例中,可以向位元組碼中輸出String型別的@Foo註解,然後由庫程式碼使用:

有關如何在位元組碼中輸出型別註解的更多細節,請查閱Kotlin 1.3.70版本釋出博文的相應部分。

Kotlin/JS

對於 Kotlin/JS,該里程碑版本包括對Gradle DSL的部分更改,它是第一個包含支援最佳化和新特性的新 IR 編譯器後端的版本。

Gradle DSL的變化

kotlin.js多平臺Gradle外掛中,引入了新的重要配置。在build.gradle.kts檔案的目標塊內,可以透過produceExecutable()設定以在構建過程中生成.js元件。

如果你正在編寫Kotlin/JS庫,則可以省略produceExecutable()。當使用新的IR編譯器後端(後文有該內容的更多相關詳細資訊)時,省略該設定意味著不會生成可執行的JS檔案(因此,構建過程將更快)。同時會在build/libs資料夾中生成一個klib檔案,該檔案可被其他Kotlin/JS專案使用,也能被同一專案所依賴。這是你不明確指定produceExecutable()時的預設行為。

使用produceExecutable()會生成JavaScript生態可執行的程式碼,無論是以其本身作為入口還是作為JavaScript庫。這會生成實際的JavaScript檔案,這些檔案可以在節點直譯器中執行,可以嵌入HTML頁面並在瀏覽器中執行,或用作JavaScript專案的依賴。

請注意,指定新的IR編譯器後端時(下文將介紹更多資訊),produceExecutable()將始終為每個目標生成一個獨立的.js檔案。當前不支援在多個生成的元件之間進行重複資料刪除或拆分程式碼。你可以期待produceExecutable()後續里程碑版本中該行為會有所變化。而該配置項的命名將來也會變更。

新的編譯器後端

Kotlin 1.4-M1是第一個包含新IR編譯器後端(面向Kotlin/JS)的版本。該後端是進行大最佳化的基礎,也是決定Kotlin/JS與JavaScript及TypeScript的互動方式發生變化的因素。下文強調的特性均針對新的IR編譯器後端。雖然預設未啟用,但我們建議你在專案中去嘗試,並開始為新的後端準備你的程式碼庫,當然希望能給我們提供反饋並記錄遇到的任何問題

Using the new backend

要開始使用新的後端,請在gradle.properties檔案中如下配置:

如果需要為IR編譯器後端和預設後端生成程式碼庫,則可以將其配置項設為both。該配置的確切作用在標題為“Both-mode”的博文中有所介紹。由於新舊編譯器後端非二進位制相容,因此該標誌是必需的。

二進位制不相容

新的IR編譯器後端的主要變化是與預設後端沒有二進位制相容性。 Kotlin/JS的兩個後端之間缺乏這種相容性,這意味著透過新的IR編譯器後端建立的庫無法在預設後端中使用,反之亦然。

如果要為你的專案使用IR編譯器後端,則需要將所有Kotlin依賴更新為支援新後端的版本。JetBrains面向Kotlin/JS釋出的針對Kotlin 1.4-M1的庫已經包含新IR編譯器後端執行所需要的所有元件。當依賴於這些庫時,Gradle會自動選擇正確的元件(無需指定IR的型別)。請注意,某些庫(例如kotlin-wrappers)在新的IR編譯器後端中存在一些問題,因為它們依賴於預設後端的專屬特性。我們已經知道這一問題,並將在未來著力去改善。

如果你是庫的作者,希望瞭解當前編譯器後端以及新的IR編譯器後端的相容性,請另外閱讀該博文的“Both-mode”部分。

下一節將詳細介紹新編譯器帶來的好處和區別。

DCE最佳化

與預設後端相比,新的IR編譯器後端能夠進行更積極的最佳化。生成的程式碼與靜態分析器配合使用有更好的效果,甚至可以透過Google的Closure Compiler從新的IR編譯器後端執行生成的程式碼,並使用高階最佳化模式(請注意Kotlin/JS Gradle外掛未為其提供具體支援)。

最明顯變化是生成元件的程式碼體積。一種消除無用程式碼的改進方案使元件大幅度精簡。例如“Hello, World!”的Kotlin/JS程式小於1.7 KiB。而對於更復雜的(演示)專案,即便是使用了kotlinx.coroutines示例專案,其數字也已經發生了巨大變化,希望你能親自去體驗:

DEFAULT BACKENDIR BACKEND
After compilation3.9 MiB1.1 MiB
After JS DCE713 KiB430 KiB
After bundle329 KiB184 KiB
After ZIP74 KiB40 KiB

如果你對此有所懷疑,請嘗試一下。預設情況下,在Kotlin 1.4-M1中,兩個後端都啟用了DCE和bundling!

匯出宣告到JavaScript

使用IR編譯器後端時,將不再自動匯出標記為public的宣告(名稱混淆過的版本也不會匯出)。這是因為IR編譯器的closed-world模型的設計是,要匯出的宣告需要進行特別的註解,這是有助於上述最佳化的其中一個因素。

要讓頂層宣告可從外部的JavaScript或TypeScript中訪問,請使用@JsExport註解。在以下示例中,我們讓KotlinGreeter(及其方法)和farewell()在JavaScript可訪問,但secretGreeting()只在Kotlin可訪問:

預覽: TypeScript定義

我們很高興展示新Kotlin/JS IR編譯器的另一個特性,就是從Kotlin程式碼生成TypeScript定義。當在開發混合應用程式時,JavaScript工具和IDE可以透過這些定義來實現自動補全,同時支援靜態分析器,並使JS和TS專案中混合Kotlin程式碼更加容易。

對於使用了ProduceExecutable()配置的專案中標有@JsExport的頂級宣告(見上文),將生成帶有TypeScript定義的.d.ts檔案。對於上面的程式碼段,它們如下所示:

在Kotlin 1.4-M1中,可以在build/js/packages/package_name/kotlin中找到這些宣告,以及相應未打包的JavaScript程式碼。請注意,由於只是預覽,因此預設情況下它們暫不會被新增到distributions資料夾中。當然未來可能會有變化。

Both-mode

了使庫的維護者能更輕鬆地遷移到新的IR編譯器後端,gradle.properties引入了kotlin.js.compiler配置:

both模式下,會同時使用IR編譯器後端和預設編譯器後端(因此命名)從程式碼源集來構建庫。這意味著Kotlin IR的klib檔案和預設編譯器的js檔案將同時生成。若釋出到同一個Maven座標時,Gradle將根據用例自動選擇合適的元件-js用於舊編譯器,klib用於新編譯器。這表示針對已經升級到Kotlin 1.4-M1並且使用兩個編譯器後端其中之一的專案,你可以使用新的IR編譯器後端編譯和釋出庫了。它可以確保在使用你的庫及預設後端的使用者體驗不被破壞,因為他們已經將專案升級到了 1.4-M1。

注意使用both模式構建依賴當前專案時,仍然存在導致IDE無法正確解析庫引用的問題。我們已意識到這個問題,並將儘快解決。

Kotlin/Native

預設支援Objective-C泛型

Kotlin的早期版本互操作中為Objective-C的泛型提供了實驗性支援。要讓Kotlin程式碼生成具有泛型的框架頭,必須使用-Xobjc-generics的編譯器選項。在1.4-M1中已預設啟用。在某些情況下,這可能會破壞現有呼叫了Kotlin框架的Objective-C或Swift程式碼。要在不使用泛型的情況下編寫框架頭,請新增-Xno-objc-generics編譯器選項。

請注意,文件中列出的所有細節和限制仍然有效。

Objective-C/Swift互操作中異常處理的變更

在1.4中,我們略微更改了從Kotlin生成的關於異常轉換方面的Swift API。 Kotlin和Swift在異常處理上存在根本差異。Kotlin沒有異常檢查,而Swift只檢查error。因此,為了使Swift程式碼能捕獲預期的異常,應該使用@Throws註解標記Kotlin函式,該註解羅列了潛在的異常型別。 當編譯成Swift或Objective-C框架時,被@Throws或其子類標記的函式在Objective-C中表示為NSError \*,而在Swift中表示為throws方法。 此前,除RuntimeExceptionError以外的任何異常都作為NSError傳遞。在1.4-M1我們對此行為作了改動。現在,只有僅被@Throws註解標記的類例項(或其子類)引發異常才丟擲NSError。而Swift/Objective-C的其他Kotlin異常均被視為未處理,並導致程式終止。

效能改進

我們一直在努力提高Kotlin/Native編譯和執行的整體效能。在1.4-M1中,我們為你提供了新的物件分配器,該物件分配器在某些基準測試中的執行速度提高了兩倍。當前,新的分配器是實驗性的,不在預設情況下使用。你可以透過-Xallocator=mimalloc編譯器選項切換至分配器。

相容性

請注意在某些極端情況下,Kotlin 1.4不向後相容1.3。語言委員會仔細審查了所有該類情況,並將其列入“相容性指南”(類似於這一篇)。現在你可以在YouTrack中找到該列表。

過載的解析規則可能會略有變化。如果你有多個相同名稱但不同簽名的函式,則在Kotlin 1.4中呼叫可能會與在Kotlin 1.3中選擇的函式有所不同。但這僅在某些極端情況下會發生,並且我們希望在實踐中這種情況儘量少發生。我們還假設在實踐中,過載函式會有類似行為,最終會互相呼叫,這就是為什麼這些更改不會影響程式的行為。但如果你想透過泛型和不同層級的多個過載編寫複雜的程式碼,請留意這一點。所有該類情況都會羅列在上述相容性指南中。

發行前須知

請注意,向後相容性不保證包含預發行版本。其特性和API可能在後續版本中更改。當到達最終RC時,編譯器將禁止所有預發行版本產生的二進位制檔案,並且將需要你重新編譯1.4‑Mx編譯過的所有內容。

如何嘗試

同樣的,你可以在play.kotl.in使用線上Kotlin

IntelliJ IDEAAndroid Studio,你可以更新Kotlin外掛到1.4-M1版本。操作指南

如果要在現有專案使用預覽版,則需要在Gradle或Maven中為預覽版本配置構建

你可以在Github發行頁下載命令列編譯器

你可以使用隨版本發行的的以下版本:

這裡檢視有關發行版的更多細節和相容庫列表。

分享你的反饋

如果你發現錯誤並向我們的問題跟蹤器YouTrack進行報告,我們表示非常感謝。我們嘗試在最終發行版前解決所有重要問題,這意味著無需等到下一個Kotlin發行版你的問題便能得到解決。

如果你有任何疑問並想參與討論,歡迎加入Kotlin Slack(在這裡獲得邀請)的#eap頻道。在該頻道中,你還可以獲取有關新預覽版的通知。

Let’s Kotlin!

其他貢獻者

我們要特別感謝Zac Sweers為Proguard配置嵌入到kotlin-reflect中所做的貢獻。

同樣感謝提交的PR合併到該版本中的所有其他貢獻者:

相關文章