跳到主要内容

Kotlin 2.2.0 现已发布!又有哪些万众瞩目的特性横空出世?

· 阅读需 10 分钟
法欧特斯卡雷特
可爱小猫咪一枚呀

大家吼!就在昨天,Kotlin v2.2.0 发布了!撒花撒花*★,°:.☆( ̄▽ ̄)/$:.°★* 。 更新的内容也已经在官网上更新:What's new in Kotlin 2.2.0 。 那么接下来,就让我来看看哪些是我最喜欢的新特性吧~!

注意!这里主要阐述的是一些我认为不错的或感兴趣的标准库和语言特性中的变化,如果你想了解完整而全面的更新内容,可以去官网看看喔~

语言特性

context parameters

首先来看看语言特性,上来第一个就是大家期待已久的新特性:context parameters!我记得它最早的时候是 context receivers, 随后然后在大概 2.1.x 的时候调整为了现在的 context parameters,也就是上下文参数。

(写编译器插件的时候总是能看到这东西变来变去的)

我们来看看官方给的示例:

// UserService defines the dependency required in the context
interface UserService {
fun log(message: String)
fun findUserById(id: Int): String
}

// Declares a function with a context parameter
context(users: UserService)
fun outputMessage(message: String) {
// Uses log from the context
users.log("Log: $message")
}

// Declares a property with a context parameter
context(users: UserService)
val firstUser: String
// Uses findUserById from the context
get() = users.findUserById(1)

当然,也支持使用无效的占位名称:

// Uses "_" as context parameter name
context(_: UserService)
fun logWelcome() {
// Finds the appropriate log function from UserService
outputMessage("Welcome!")
}

这个特性可以大大增加对于特定作用域进行扩展的灵活性和便捷性。举个典型的例子,在 Jetpack Compose 或者 Compose Multiplatform 中, 原本的很多特定作用域函数都是通过带有 receiver 的函数和接口实现完成的,比如这个:

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

// ...
}

这是 compose 中的 RowScope 中的一部分,可以看到它约束只有在 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
}

// Resolves enum entries based on the known type of 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 // Constructor parameter
@field:Email // Backing field
@get:Email // Getter method
@property:Email // Kotlin property reference
val email: String,
) {
@field:Email
@get:Email
@property:Email
val secondaryEmail: String? = null
}

而现在这个特性就在某些特定场景下简化了这种繁琐操作:使用 @all

data class User(
val username: String,

// Applies @Email to param, property, field,
// get, and set_param (if var)
@all:Email val email: String,
) {
// Applies @Email to property, field, and getter
// (no param since it's not in the constructor)
@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) {
    // Branch with only the primary condition. Calls `feedDog()` when `animal` is `Dog`
    is Animal.Dog -> animal.feedDog()
    // Branch with both primary and guard conditions. Calls `feedCat()` when `animal` is `Cat` and is not `mouseHunter`
    is Animal.Cat if !animal.mouseHunter -> animal.feedCat()
    // Prints "Unknown animal" if none of the above conditions match
    else -> println("Unknown animal")
    }
    }
  • 非本地的 breakcontinue。 换句话说,就是在一个没有纯粹的 inline DSL 中也可以使用 breakcontinue
  • $ 字符串插值 它们真的很喜欢美元符号。

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

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 的整合。

你有什么看法呢? (o゜▽゜)o☆