Kotlin 2.2.0发布啦,新特性闪亮登场!

208 阅读4分钟

前言

嘿,各位Kotlin爱好者们!就在最近,Kotlin v2.2.0像一颗超级新星般闪耀发布啦!撒花庆祝🎉🎉🎉 更新内容已经在官网闪亮登场:What's new in Kotlin 2.2.0 。接下来,就让我化身“特性探险家”,带大家瞧瞧那些让我眼前一亮的新特性吧!

不过先提醒一下哈,这里主要聚焦一些我觉得超棒或者超有趣的标准库和语言特性变化。要是你想来个全面大揭秘,官网的大门随时为你敞开哟~

0be9fc22988662ef312e46a19c021f69.jpeg

语言特性大揭秘

context parameters:作用域扩展的“魔法钥匙”

先来看看语言特性里的“重头戏”——context parameters!它可是从context receivers“进化”而来的,在2.1.x版本的时候就悄悄变身为现在的模样啦。(写编译器插件的时候,感觉它就像个调皮的小精灵,老是变来变去😜)

来看看官方给的示例代码:

// UserService 定义了上下文所需的依赖
interface UserService {
    fun log(message: String)
    fun findUserById(id: Int): String
}
// 声明一个带有上下文参数的函数
context(users: UserService)
fun outputMessage(message: String) {
    // 从上下文中使用 log
    users.log("Log: $message")
}
// 声明一个带有上下文参数的属性
context(users: UserService)
val firstUser: String
    // 从上下文中使用 findUserById
    get() = users.findUserById(1)

它还支持用无效的占位名称呢:

// 用 "_" 作为上下文参数名
context(_: UserService)
fun logWelcome() {
    // 从 UserService 中找到合适的 log 函数
    outputMessage("Welcome!")
}

这个特性就像是给特定作用域的扩展装上了“超级引擎”,让扩展变得更加灵活和便捷。就拿Jetpack Compose或者Compose Multiplatform来说,以前很多特定作用域函数都是靠带有receiver的函数和接口来实现的。比如Compose里的RowScope:

@LayoutScopeMarker
@Immutable
@JvmDefaultWithCompatibility
interface RowScope {
    @Stable
    fun Modifier.weight(
        weight: Float,
        fill: Boolean = true
    ): Modifier
    // ...
}

可以看到,它限制了只有在Row { }的作用域内才能使用Modifier.weigth。以前这类对作用域有要求的函数都是通过接口的receiver function实现的,现在有了context parameters,就能轻松对具体作用域进行扩展啦!

不过呢,这里只是举了个“限制作用域”的例子,到底是内部实现还是用context parameters,还得根据实际情况来“见招拆招”。目前这个特性还在实验阶段,只要在编译器参数里加上 -Xcontext-parameters 就能启用它啦,赶紧试试吧!

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xcontext-parameters")
    }
}

context-sensitive resolution:代码简化的“智能助手”

老实说,这个特性我之前都没怎么留意过,感觉它就像个藏在角落里的“宝藏”。简单来说,它就是“上下文敏感的解析”,能让代码变得更简洁,使用体验更棒。

来看看官方的示例:

enum class Problem {
    CONNECTION, AUTHENTICATION, DATABASE, UNKNOWN
}
fun message(problem: Problem): String = when (problem) {
    Problem.CONNECTION -> "connection"
    Problem.AUTHENTICATION -> "authentication"
    Problem.DATABASE -> "database"
    Problem.UNKNOWN -> "unknown"
}

在context-sensitive resolution的帮助下,在已知的作用域下,它能帮你省略一些不必要的类型引用:

enum class Problem {
    CONNECTION, AUTHENTICATION, DATABASE, UNKNOWN
}
// 根据已知的 problem 类型解析枚举条目
fun message(problem: Problem): String = when (problem) {
    CONNECTION -> "connection"
    AUTHENTICATION -> "authentication"
    DATABASE -> "database"
    UNKNOWN -> "unknown"
}

这个特性可以用在好多场景里,比如:

  • when的子域(就像上面的示例)
  • 显式返回类型
  • 声明的变量类型
  • 类型检测 (is) 和类型转化 (as)
  • sealed class的已知类型结构
  • 参数的声明类型

虽然一下子列举了这么多场景,可能有点让人“摸不着头脑”,不过从上面的代码示例也能看出它的主要作用啦。目前这个特性也在实验阶段,需要加上编译器参数 -Xcontext-sensitive-resolution 才能启用,感兴趣的朋友快去试试吧!

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xcontext-sensitive-resolution")
    }
}

annotation use-site targets:注解标记的“便捷神器”

还记得以前在属性上标记注解,想标记在字段、getter或参数等位置上时,那繁琐的操作吗?看看下面的代码:

data class User(
    val username: String,
    @param:Email      // 构造参数
    @field:Email      // 后端字段
    @get:Email        // Getter方法
    @property:Email   // Kotlin属性引用
    val email: String,
) {
    @field:Email
    @get:Email
    @property:Email
    val secondaryEmail: String? = null
}

现在好了,新特性在特定场景下简化了这个操作,用上了@all:

data class User(
    val username: String,
    // 将@Email应用到param、property、field、
    // get和set_param(如果是var)
    @all:Email val email: String,
) {
    // 将@Email应用到property、field和getter
    // (没有param,因为它不在构造器中)
    @all:Email val secondaryEmail: String? = null
}

用了@all之后,它就会根据实际情况,给所有可能的位置都添加上指定的注解啦。至于具体的逻辑和限制,感兴趣的小伙伴可以去官网深入研究一番。目前这个特性也在实验阶段,需要加上编译器参数 -Xannotation-target-all 才能启用哦!

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xannotation-target-all")
    }
}

defaulting rules for use-site annotation targets:注解默认位置的“规则制定者”

和上一个特性类似,这也是和use-site annotation target(注解作用位置)相关的特性,它能指定注解默认生效的位置。

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xannotation-default-target=param-property")
    }
}

或者

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xannotation-default-target=first-only")
    }
}

Support for nested type aliases:嵌套类型别名的“新玩法”

如标题所说,这个新特性支持在嵌套的结构层次中使用alias创建别名类型啦。以前可能都没怎么尝试过这种用法,原来以前还不行呢😆

class Dijkstra {
    typealias VisitedNodes = Set<Node>
    private fun step(visited: VisitedNodes, ...) = ...
}

当然啦,这个特性也有一些限制,感兴趣的朋友可以去官网详细了解。同样,它也是实验性特性,加上编译器参数 -Xnested-type-aliases 就能启用。

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xnested-type-aliases")
    }
}

稳定特性来袭

接下来,文档里列举了几个在当前版本转为稳定的特性:

when子域的守卫条件

在when的条件里可以用if编写更灵活的条件啦,就像这样:

sealed interface Animal {
    data class Cat(val mouseHunter: Boolean) : Animal {
        fun feedCat() {}
    }
    data class Dog(val breed: String) : Animal {
        fun feedDog() {}
    }
}
fun feedAnimal(animal: Animal) {
    when (animal) {
        // 只有主要条件的分支。当animal是Dog时调用feedDog()
        is Animal.Dog -> animal.feedDog()
        // 有主要条件和守卫条件的分支。当animal是Cat且不是mouseHunter时调用feedCat()
        is Animal.Cat if !animal.mouseHunter -> animal.feedCat()
        // 如果以上条件都不匹配,打印"Unknown animal"
        else -> println("Unknown animal")
    }
}

非本地的break和continue

也就是说,在一个没有纯粹inline DSL的地方也能用break和continue啦,是不是很方便!

双$字符串插值

看来Kotlin团队真的很喜欢美元符号😜

Kotlin/JVM的惊喜

本来想跳过一些编译器更新和非标准库的Kotlin/JVM部分,不过我发现了一些超有趣的东西。

接口函数更改使用default

早期版本的Kotlin/JVM实现接口中的函数可不是直接用Java的default function特性的,随着版本不断更新,这个行为也在慢慢变化。现在,编译器参数 -Xjvm-default 被废弃,变成了默认行为。

支持在Kotlin metadata中读写注解

这个其实也没啥特别要说的,就是更新了Kotlin metadata JVM library库的内容,现在可以通过它来读写注解相关的东西啦。

不过结合之前Kotlin和Spring团队宣布合作的事情,难道这是在为更高效的反射库做准备?🤔

改进Java互操作,支持内联值类

这个特性可太让我兴奋啦!它改进了Java对@JvmInline value class的互操作性,还新增了一个实验性注解@JvmExposeBoxed,能让value class在Java中更好用。

看看官方例子:

@JvmInline
value class MyInt(val value: Int)
fun MyInt.timesTwoBoxed(): MyInt = MyInt(this.value * 2)

以前,函数timesTwoBoxed()的receiver是个value class,会被编译成Java不可访问的函数形式。现在,加上注解:

@JvmExposeBoxed
@JvmInline
value class MyInt(val value: Int)
@JvmExposeBoxed
fun MyInt.timesTwoBoxed(): MyInt = MyInt(this.value * 2)

再回到Java中调用:

MyInt input = new MyInt(5);
MyInt output = ExampleKt.timesTwoBoxed(input);

哇塞,这代码简直太丝滑了!回想起以前写库的时候,为了把value class相关的函数巧妙转化或暴露,那叫一个头疼,现在这个问题轻松解决啦!当然,如果不想每个地方都加注解,也可以加上编译器参数 -Xjvm-expose-boxed 来标记整个模块。而且这种变化只对外的Java符号生效,Kotlin模块内部还是保持高效内联,完全不用担心性能问题。

改进JVM records的注解支持

这和之前提到的注解可以用@all的内容有点关系。在Java中,注解的Target有RECORD_COMPONENT选项,而Kotlin没有。以前想让Kotlin的注解支持RECORD_COMPONENT可麻烦了。

现在,用@all在一个@JvmRecord的属性上,就能在支持RECORD_COMPONENT的前提下附加注解啦。

@JvmRecord
data class Person(val name: String, @all:Positive val age: Int)

Kotlin/Native、Kotlin/Wasm和Kotlin/JS的动态

Kotlin/Native

native相关的内容,主要还是在内存管理、性能优化等方面努力,还顺便废弃了Window 7 target。要是你感兴趣,就去官方文档看看吧~

Kotlin/Wasm & Kotlin/JS

希望Kotlin/Wasm和Kotlin/JS能快速发展呀,我以后转全栈可就靠它们啦😆 这两个端的更新不是很多,感兴趣的小伙伴可以去官方文档简单了解一下。

标准库的稳定更新

跳过中间的一些内容,比如Gradle的更新,来看看标准库。标准库主要稳定了两个多平台API:

  • Base64的编码/解码
  • HexFormat相关的hex API

这些都是很常用的东西,稳定了可太好了!

Compose compiler的小变化

看来Compose并入Kotlin编译器之后,每次更新都得带着它啦。我对这方面不太擅长,就不多说啦,有需要的小伙伴自己去官网看看吧。

尾声

总的来说,2.2.0版本的更新可真不少,有不少我觉得超有用的东西,尤其是context parameters这个实验性特性的亮相,还有Gradle中Binary compatibility validation的整合。

你有什么看法呢?欢迎留言讨论哟~ 😎