Kotlin 1.6 正式釋出,帶來哪些新特性?

語言: CN / TW / HK

theme: smartblue highlight: github


11月16日,Kotlin 1.6 正式對外發布,這個版本中都有哪些新的語法特性?

  1. 更安全的when語句(exhaustive when statements)
  2. 掛起函式型別可作父類 (suspending functions as supertypes )
  3. 普通函式轉掛起函式(suspend conversion)
  4. Builder函式更加易用
  5. 遞迴泛型的型別推導
  6. 註解相關的一些優化

1. 更安全的 when 語句

Kotlin 的 when 關鍵字允許我們在 case 分支中寫表示式或者語句。1.6 之前在 case 分支寫語句時存在安全隱患:

```kotlin // 定義列舉 enum class Mode { ON, OFF } val x: Mode = Mode.ON

// when表示式 val result = when(x) { Mode.ON -> 1 // case 中是一個表示式 Mode.OFF -> 2 }

// when語句 when(x) { Mode.ON -> println("ON") // case 是一個語句 Mode.OFF -> println("OFF") } ``` 下表說明了編譯器針對 when 關鍵字的檢查內容

|x 的型別| 列舉、密封類/介面、Bool型等(可窮舉型別)|不可窮舉型別 | |--|--|--| |when表示式|case 必須窮舉所有分支,或者新增 else,否則編譯出錯|Case 分支必須包含 else,否則編譯出錯| |when語句|case 可以不窮舉所有分支,不會報錯|同上|

可見,當 x 是可窮舉型別時,編譯器對when表示式的檢查比較嚴謹,如果 case 不能窮舉所有分支或者缺少 else,編譯器會報錯如下: ERROR: 'when' expression must be exhaustive, add necessary 'is TextMessage' branch or 'else' branch instead

但編譯器對於 when語句 的檢查卻不夠嚴謹,即使沒有窮舉所有分支也不會報錯,不利於開發者寫出安全的程式碼: kotlin // when語句 when(x) { // WARNING: [NON_EXHAUSTIVE_WHEN] 'when' expression on enum is recommended to be exhaustive, add 'OFF' branch or 'else' branch instead Mode.ON -> println("ON") // case 是一個語句 } Kotlin 1.6 起,當你在 When語句 中是可窮舉型別時必須處理所有分支,不能遺漏。考慮到歷史程式碼可能很多,為了更平穩的過渡,1.6 對 when語句 中沒有窮舉的 case 會首先給出 Warning,從 1.7 開始 Warning 將變為 Error 要求開發者強制解決。

2. 掛起函式型別可作父類

Kotlin 中一個函式型別可以作為父類被繼承。 ```kotlin class MyFun(var param: P): () -> Result { override fun invoke(): Result { // 基於成員 param 自定義邏輯 } }

fun handle(handler: () -> Result) { //... } ```

Kotlin 程式碼中大量使用各種函式型別,許多方法都以函式型別作為引數。當你需要呼叫這些方法時,需要傳入一個函式型別的例項。而當你想在例項中封裝一些可複用的邏輯時,可以使用函式型別作為父類建立子類。

但是這種做法目前不適用於掛起函式,你無法繼承一個 suspend 函式型別的父類

```kotlin class C : suspend () -> Unit { // Error: Suspend function type is not allowed as supertypes }

C().startCoroutine(completion = object : Continuation { override val context: CoroutineContext get() = TODO("Not yet implemented")

override fun resumeWith(result: Result<Unit>) {
    TODO("Not yet implemented")
}

}) ```

但是以掛起函式作為引數或者 recevier 的方法還挺多的,所以 Kotlin 1.5.30 在 Preveiw 中引入了此 feature,這次 1.6 將其 Stable。 ```kotlin class MyClickAction : suspend () -> Unit { override suspend fun invoke() { TODO() } }

fun launchOnClick(action: suspend () -> Unit) {} ```

如上,你可以現在可以像這樣呼叫了 launchOnClick(MyClickAction())

需要注意普通函式型別作為父類是可以多繼承的 ```kotlin class MyClickAction : () -> Unit, (View) -> Unit { override fun invoke() { TODO("Not yet implemented") }

override fun invoke(p1: View) {
    TODO("Not yet implemented")
}

} ``` 但是目前掛起函式作為父類不支援多繼承,父類列表中,既不能出現多個 suspend 函式型別,也不能有普通函式型別和suspend函式型別共存。

3. 普通函式轉掛起函式

這個 feature 也是與函式型別有關。

Kotlin 中為一個普通函式新增 suspend 是無害的,雖然編譯器會提示你沒必要這麼做。當一個函式簽名有一個 suspend 函式型別引數,但是也允許你傳入一個普通函式,在某些場景下是非常方便的。 ```kotlin //combine 的 transform 引數是一個 suspend 函式 public fun combine( flow: Flow, flow2: Flow, transform: suspend (a: T1, b: T2) -> R): Flow = flow.combine(flow2, transform)

suspend fun before4_1() { combine( flowA, flowB ) { a, b -> a to b }.collect { (a: Int, b: Int) -> println("$a and $b") } } ```

如上述程式碼所示,flowcombine 方法其引數 transform 型別是一個 suspend 函式,我們希望再次完成一個 Pair 的建立。這個簡單的邏輯本無需使用 suspend ,但在 1.4 之前只能像上面這樣寫。

Kotlin 1.4 開始,普通函式的引用可以作為 suspend 函式傳參,所以 1.4 之後可以改成下面的寫法,程式碼更簡潔: kotlin suspend fun from1_4() { combine( flowA, flowB, ::Pair ).collect { (a: Int, b: Int) -> println("$a and $b") } }

1.4 之後仍然有一些場景中,普通函式不能直接轉換為 suspend 函式使用

```kotlin fun getSuspending(suspending: suspend () -> Unit) {}

fun suspending() {}

fun test(regular: () -> Unit) { getSuspending { } // OK getSuspending(::suspending) // OK from 1.4 getSuspending(regular) // NG before 1.6 } 比如上面 `getSuspending(regular)` 會報錯如下:cmd ERROR:The feature "suspend conversion" is disabled ```

Kotlin 1.6 起,所有場景的普通函式型別都可以自動轉換為 suspend 函式傳參使用,不會再看到上述錯誤。

4. Builder 函式更加易用

我們在構建集合時會使用一些 Builder函式,比如 buildListbuildMap 之類。 ```kotlin @ExperimentalStdlibApi @kotlin.internal.InlineOnly public inline fun buildList(@BuilderInference builderAction: MutableList.() -> Unit): List { contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } return buildListInternal(builderAction) }

@kotlin.ExperimentalStdlibApi val list = buildList { add("a") add("b") } ```

buildList 的實現中使用 @BuilderInterface 註解了 builderAction 這個 lambda 。這樣可以在呼叫時 buildList 通過 builderAction 內部的方法呼叫智慧推匯出泛型引數的型別,從而減少模板程式碼 ```kotlin // 可省略 val list = buildList { add("a") add("b") }

// 不可省略 val list = buildList { add("a") add("b") val x = get(1) } ```

但是 BuilderInterface 的型別推導限制比較多,比如 lambda 中呼叫的方法的簽名要求比較嚴格,必須引數是泛型且返回值沒有泛型,破壞了規則,型別推導失敗了。所以上面程式碼中 lambda 有 get() 呼叫時,就必須清楚的標記泛型型別。這使得集合類的 builder 函式使用起來不那麼靈活。

Kotlin 1.6 起 BuilderInterface 沒有了類似限制,對我們來說最直觀好處就是 Builder 函式內怎樣的呼叫都不會受限制,使用更加自由 ```kotlin val list = buildList { add("a") add("b") set(1, null) //OK val x = get(1) //OK if (x != null) { removeAt(1) //OK } }

val map = buildMap { put("a", 1) //OK put("b", 1.1) //OK put("c", 2f) //OK } ```

此 feature 在 1.5.30 也可以通過 新增 -Xunrestricted-builder-inference 編譯器選項生效,1.6 已經是預設生效了。

5. 遞迴泛型的型別推導

這個 feature 我們平常需求比較少。

Java 或者 Kotlin 中我們可以像下面這樣定義有遞迴關係的泛型,即泛型的上限是它本身 java public class PostgreSQLContainer<SELF extends PostgreSQLContainer<SELF>> extends JdbcDatabaseContainer<SELF> { //... } 這種情況下的型別推導比較困難,Kotlin 1.5.30 開始可以只基於泛型的上線進行型別推導。

```kotlin // Before 1.5.30 val containerA = PostgreSQLContainer(DockerImageName.parse("postgres:13-alpine")).apply { withDatabaseName("db") withUsername("user") withPassword("password") withInitScript("sql/schema.sql") }

// With compiler option in 1.5.30 or by default starting with 1.6.0 val containerB = PostgreSQLContainer(DockerImageName.parse("postgres:13-alpine")) .withDatabaseName("db") .withUsername("user") .withPassword("password") .withInitScript("sql/schema.sql") ```

1.5.30 支援此 feature 需要新增 -Xself-upper-bound-inference 編譯選項, 1.6 開始預設支援。

6. 註解相關的一些優化

Kotlin 1.6 中對註解進行了諸多優化,在編譯器註解處理過程中將發揮作用

支援註解的例項化

kotlin annotation class InfoMarker(val info: String) fun processInfo(marker: InfoMarker) = ... fun main(args: Array<String>) { if (args.size != 0) processInfo(getAnnotationReflective(args)) else processInfo(InfoMarker("default")) } Java 的註解本質是實現了 Annotation 的介面,可以被繼承使用

```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface JavaClassAnno { String[] value(); }

public interface JavaClassAnno extends Annotation{ //... }

class MyAnnotation implements JavaClassAnno { // <--- works in Java //... } 但是在 Kotlin 中無法繼承使用,這導致有一些接受註解類的 API 在 Kotlin 側無法呼叫。java class MyAnnotationLiteral : JavaClassAnno { // <--- doesn't work in Kotlin (annotation can not be inherited) //... } ``` 註解類可以例項化之後,可以呼叫接收註解類引數的 API,能夠與 Java 程式碼進行更好地相容

泛型引數可添加註解

```kotlin @Target(AnnotationTarget.TYPE_PARAMETER) annotation class BoxContent

class Box<@BoxContent T> {} ```

Kotlin 1.6 之後可以為泛型引數添加註解,這將為 KAPT / KSP 等註解處理器中提供方便。

可重複的執行時註解

Jdk 1.8 引入了 @java.lang.annotation.Repetable 元註解,允許同一個註解被新增多次。 Kotlin 也相應地引入了 @kotlin.annotation.Repeatable ,不過 1.6之前只能註解 @Retention(RetentionPolicy.SOURCE) 的註解,當非 SOURCE 的註解出現多次時,會報錯 ERROR: [NON_SOURCE_REPEATED_ANNOTATION] Repeatable annotations with non-SOURCE retention are not yet supported

此外,Kotlin 側程式碼也不能使用 Java 的 @Repeatable 註解來註解多次。

Kotlin1.6 開始,取消了只能用在 SOURCE 類註解的限制,任何型別的註解都可以出現多次,而且 Kotlin 側支援使用 Java 的 @Repeatable 註解

```kotlin @Repeatable(AttributeList.class) @Target({ElementType.TYPE}) @Retentioin(RetentionPolicy.RUNTIME) //雖然是 RUNTIME 註解 annotation class Attribute(val name: String)

@Attribute("attr1") //OK @Attribute("attr2") //OK class MyClass {} ```

最後

上述介紹的是 Kotlin1.6 在語法方面的一些新特性,大部分在 1.5.30 中作為 preview 功能已經出現過,這次在 1.6 中進行了轉正。除了新的語法特性,1.6 在各平臺 Compiler 上有諸多新內容,我們在平日開發中接觸不到本文就不介紹了。

更多內容參考:https://kotlinlang.org/docs/whatsnew16.html