Kotlin 2.0.0 的新特性

Kotlin 2.0.0 的新特性

Kotlin 2.0.0 已发布, 新 Kotlin K2 编译器 已稳定! 此外, 还有一些其他亮点:

IDE 支持

最新的 IntelliJ IDEA 和 Android Studio 捆绑了支持 Kotlin 2.0.0 的 Kotlin 插件. 你无需更新 IDE 中的 Kotlin 插件. 你只需在构建脚本中更改 Kotlin 版本为 Kotlin 2.0.0.

  • 有关IntelliJ IDEA支持Kotlin K2编译器的详细信息, 请参阅IDE中的支持.
  • 有关 IntelliJ IDEA 支持 Kotlin 的详细信息, 请参阅 Kotlin releases.

Kotlin K2 编译器

通往 K2 编译器的道路是漫长的, 但现在 JetBrains 团队已准备好宣布其稳定化. 在 Kotlin 2.0.0 中, 默认使用新的 Kotlin K2 编译器, 它对所有目标平台的已稳定: JVM, Native, Wasm 和 JS. 新编译器带来了重大的性能改进, 加快了新语言功能的开发, 统一了 Kotlin 支持的所有平台, 并为多平台项目提供了更好的架构.

JetBrains 团队从选定的用户和内部项目中成功编译了 1000 万行代码, 确保了新编译器的质量. 18,000 名开发人员和 80,000 个项目参与了稳定化过程, 在他们的项目中试用了新的 K2 编译器, 并报告了他们发现的任何问题.

为了使向新编译器的迁移过程尽可能顺利, Jetbrains创建了K2 编译器迁移指南. 该指南解释了编译器的诸多优点, 强调了你可能遇到的任何变化, 并介绍了必要时如何回退到以前的版本.

Jetbrains在一篇博文中探讨了 K2 编译器在不同项目中的性能. 如果你想查看有关 K2 编译器性能的真实数据, 并了解如何从自己的项目中收集性能基准, 请查看该博文.

当前 K2 编译器的限制

在Gradle项目中启用K2会有一些限制, 在以下情况下会影响到使用8.3以下版本Gradle的项目:

  • buildSrc 编译源代码.
  • 在包含的构建中编译 Gradle 插件.
  • 在 Gradle 版本低于 8.3 的项目中编译其他 Gradle 插件.
  • 编译 Gradle 插件依赖.

如果遇到上述问题, 可以采取以下措施解决:

  • buildSrc和任何 Gradle 插件及其依赖设置语言版本:

    bash 复制代码
    kotlin {
        compilerOptions {
            languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
            apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
        }
    }

    如果为特定任务配置了语言和 API 版本, 这些值将覆盖 compilerOptions 扩展设置的值. 在这种情况下, 语言和 API 版本不应高于 1.9.

  • 将项目中的 Gradle 版本更新到 8.3 或更高版本.

对智能类型转换的改进

Kotlin 编译器能在特定情况下自动将对象转换为某种类型, 省去了自己显式转换的麻烦. 这就是所谓的 智能类型转换. 现在, Kotlin K2 编译器能在比以前更多的情况下执行智能类型转换.

在 Kotlin 2.0.0 中, Jetbrains在以下方面对智能类型转换进行了改进:

  • 局部变量及其作用域
  • 使用逻辑or操作符进行类型检查
  • 内联函数
  • 带有函数类型的属性
  • 异常处理
  • 递增和递减操作符

局部变量及其作用域

以前, 如果一个变量在一个 if 条件中被评估为非 null, 该变量将被智能转换. 然后, 有关该变量的信息将在 if 块的作用域内进一步共享.

但是, 如果在 if 条件之外 声明了变量, 那么在 if 条件中就没有关于该变量的任何信息, 因此无法进行智能转换. 这种行为也出现在 when 表达式和 while 循环中.

从 Kotlin 2.0.0 开始, 如果你在if, whenwhile条件中使用变量之前先声明该变量, 那么编译器收集到的关于该变量的任何信息都可以在相应的代码块中访问, 以便进行智能转换.

当你想把布尔条件提取到变量中时, 这将非常有用. 然后, 你可以给变量取一个有意义的名字, 这将提高代码的可读性, 并使以后在代码中重复使用变量成为可能. 例如:

kotlin 复制代码
class Cat {
    fun purr() {
        println("Purr purr")
    }
}

fun petAnimal(animal: Any) {

    val isCat = animal is Cat
    if (isCat) {
        // In Kotlin 2.0.0, the compiler can access
        // information about isCat, so it knows that
        // animal was smart-cast to the type Cat.
        // Therefore, the purr() function can be called.
        // In Kotlin 1.9.20, the compiler doesn't know
        // about the smart cast, so calling the purr()
        // function triggers an error.
        animal.purr()
    }
}

fun main() {

    val kitty = Cat()
    petAnimal(kitty)
    // Purr purr
}

使用逻辑或操作符进行类型检查

在 Kotlin 2.0.0 中, 如果将对象的类型检查与or操作符 (||)结合在一起, 则会将它们智能转换为最接近的共同超类型. 在此更改之前, 智能投向总是投向 Any 类型.

在这种情况下, 在访问对象的任何属性或调用其函数之前, 仍需手动检查对象类型. 例如:

kotlin 复制代码
interface Status {
    fun signal() {}
}

interface Ok : Status
interface Postponed : Status
interface Declined : Status

fun signalCheck(signalStatus: Any) {
    if (signalStatus is Postponed || signalStatus is Declined) {
        // signalStatus is smart-cast to a common supertype Status
        signalStatus.signal()
        // Prior to Kotlin 2.0.0, signalStatus is smart cast
        // to type Any, so calling the signal() function triggered an
        // Unresolved reference error. The signal() function can only
        // be called successfully after another type check:

        // check(signalStatus is Status)
        // signalStatus.signal()
    }
}

普通超类型是联合类型的近似 . Kotlin 不支持Union类型.

内联函数

在 Kotlin 2.0.0 中, K2 编译器以不同的方式处理内联函数, 允许它结合其他编译器分析来确定智能转换是否安全.

具体来说, 内联函数现在被视为具有隐式callsInPlace契约. 这意味着传递给内联函数的任何 lambda 函数都会被就地调用. 由于 lambda 函数是就地调用的, 因此编译器知道 lambda 函数不能泄漏对其函数体中包含的任何变量的引用.

编译器利用这一知识和其他编译器分析来决定对捕获的变量进行智能转换是否安全. 例如:

kotlin 复制代码
interface Processor {
    fun process()
}

inline fun inlineAction(f: () -> Unit) = f()

fun nextProcessor(): Processor? = null

fun runProcessor(): Processor? {
    var processor: Processor? = null
    inlineAction {
        // In Kotlin 2.0.0, the compiler knows that processor
        // is a local variable, and inlineAction() is an inline function, so
        // references to processor can't be leaked. Therefore, it's safe
        // to smart-cast processor.

        // If processor isn't null, processor is smart-cast
        if (processor != null) {
            // The compiler knows that processor isn't null, so no safe call
            // is needed
            processor.process()

            // In Kotlin 1.9.20, you have to perform a safe call:
            // processor?.process()
        }

        processor = nextProcessor()
    }

    return processor
}

函数类型的属性

在以前的 Kotlin 版本中, 有一个错误意味着带有函数类型的类属性不能进行智能转换. Jetbrains在 Kotlin 2.0.0 和 K2 编译器中修复了这一行为. 例如:

kotlin 复制代码
class Holder(val provider: (() -> Unit)?) {
    fun process() {
        // In Kotlin 2.0.0, if provider isn't null, then
        // provider is smart-cast
        if (provider != null) {
            // The compiler knows that provider isn't null
            provider()

            // In 1.9.20, the compiler doesn't know that provider isn't
            // null, so it triggers an error:
            // Reference has a nullable type '(() -> Unit)?', use explicit '?.invoke()' to make a function-like call instead
        }
    }
}

如果重载了 invoke 操作符, 这一更改也同样适用. 例如:

kotlin 复制代码
interface Provider {
    operator fun invoke()
}

interface Processor : () -> String

class Holder(val provider: Provider?, val processor: Processor?) {
    fun process() {
        if (provider != null) {
            provider()
            // In 1.9.20, the compiler triggers an error:
            // Reference has a nullable type 'Provider?' use explicit '?.invoke()' to make a function-like call instead
        }
    }
}

异常处理

在 Kotlin 2.0.0 中, Jetbrains对异常处理进行了改进, 这样就可以将智能类型转换信息传递给 catchfinally 块. 由于编译器会跟踪你的对象是否具有可空类型, 因此这一更改会使你的代码更加安全. 例如:

kotlin 复制代码
fun testString() {

    var stringInput: String? = null
    // stringInput is smart-cast to String type
    stringInput = ""
    try {
        // The compiler knows that stringInput isn't null
        println(stringInput.length)
        // 0
        // The compiler rejects previous smart cast information for 
        // stringInput. Now stringInput has the String? type.
        stringInput = null
        // Trigger an exception
        if (2 > 1) throw Exception()
        stringInput = ""
    } catch (exception: Exception) {
        // In Kotlin 2.0.0, the compiler knows stringInput 
        // can be null, so stringInput stays nullable.
        println(stringInput?.length)
        // null
        // In Kotlin 1.9.20, the compiler says that a safe call isn't
        // needed, but this is incorrect.
    }
}

递增和递减操作符

在 Kotlin 2.0.0 之前, 编译器不知道对象的类型会在使用递增或递减操作符后发生变化. 由于编译器无法准确跟踪对象类型, 你的代码可能会导致无法解决的引用错误. 在 Kotlin 2.0.0 中, 这一问题已得到修复:

kotlin 复制代码
interface Rho {
    operator fun inc(): Sigma = TODO()
}

interface Sigma : Rho {
    fun sigma() = Unit
}

interface Tau {
    fun tau() = Unit
}

fun main(input: Rho) {

    var unknownObject: Rho = input
    // Check if unknownObject inherits from the Tau interface
    if (unknownObject is Tau) {
        // Uses the overloaded inc() operator from interface Rho,
        // which smart casts the type of unknownObject to Sigma.
        ++unknownObject
        // In Kotlin 2.0.0, the compiler knows unknownObject has type
        // Sigma, so the sigma() function can be called successfully.
        unknownObject.sigma()
        // In Kotlin 1.9.20, the compiler thinks unknownObject has type
        // Tau, so calling the sigma() function is not allowed.
        // In Kotlin 2.0.0, the compiler knows unknownObject has type
        // Sigma, so calling the tau() function is not allowed.
        unknownObject.tau()
        // Unresolved reference 'tau'
        // In Kotlin 1.9.20, the compiler mistakenly thinks that 
        // unknownObject has type Tau, the tau() function can be 
        // called successfully.
    }
}

Kotlin 跨平台改进

在 Kotlin 2.0.0 中, Jetbrains对 K2 编译器中与 Kotlin Multiplatform 相关的以下方面进行了改进:

  • 编译期通用源和平台源的分离
  • expect声明和actual声明的不同可见性水平

编译期间通用源与平台源的分离

以前, Kotlin 编译器的设计使其无法在编译时将通用源代码集和平台源代码集分开. 因此, 通用代码可以访问平台代码, 这导致了不同平台之间的不同行为. 此外, 通用代码中的一些编译器设置和依赖关系也会泄漏到平台代码中.

在 Kotlin 2.0.0 中, Jetbrains实现了新的 Kotlin K2 编译器, 包括重新设计编译方案, 以确保严格分离通用和平台源代码集. 在使用 expect和actual函数时, 这一变化最为明显. 以前, 通用代码中的函数调用有可能解析为平台代码中的函数. 例如:

通用代码:

kotlin 复制代码
fun foo(x: Any) = println("common foo") 

fun exampleFunction() {     foo(42) }

平台代码:

kotlin 复制代码
// JVM 
fun foo(x: Int) = println("platform foo")  
// JavaScript 
// There is no foo() function overload 
// on the JavaScript platform

在此示例中, 根据运行平台的不同, 通用代码的行为也不同:

  • 在 JVM 平台上, 调用通用代码中的 foo() 函数会导致平台代码中的 foo() 函数被调用为 platform foo.
  • 在 JavaScript 平台上, 调用通用代码中的 foo() 函数会导致公共代码中的 foo() 函数被调用为 common foo, 因为平台代码中没有此类函数.

在 Kotlin 2.0.0 中, 通用代码无法访问平台代码, 因此两个平台都成功地将 foo() 函数解析为普通代码中的 foo() 函数: common foo.

除了改进跨平台行为的一致性外, Jetbrains还努力修复 IntelliJ IDEA 或 Android Studio 与编译器之间出现冲突的情况. 例如, 当你使用 expect和actual类时, 会出现以下情况:

通用代码:

kotlin 复制代码
expect class Identity {     
    fun confirmIdentity(): String 
}  

fun common() {     
    // Before 2.0.0,     
    // it triggers an IDE-only error     
    Identity().confirmIdentity()     
    // RESOLUTION_TO_CLASSIFIER : Expected class     
    // Identity has no default constructor. 
}

平台代码:

kotlin 复制代码
actual class Identity {     
    actual fun confirmIdentity() = "expect class fun: jvm" 
}

在本例中, expectIdentity 没有默认构造函数, 因此无法在普通代码中成功调用. 以前, 只有IDE会报告错误, 但代码仍能在 JVM 上编译成功. 但现在编译器会正确报错:

arduino 复制代码
Expected class 'expect class Identity : Any' does not have default constructor
当解析行为不改变时

Jetbrains仍在向新编译方案迁移的过程中, 因此当你调用不在同一源代码集内的函数时, 解析行为仍保持不变. 你主要会在普通代码中使用来自多平台库的重载时注意到这种差异.

假设你有一个库, 其中有两个签名不同的 whichFun()函数:

kotlin 复制代码
// Example library

// MODULE: common
fun whichFun(x: Any) = println("common function")

// MODULE: JVM
fun whichFun(x: Int) = println("platform function")

如果在通用代码中调用 whichFun()函数, 库中参数类型最相关的函数将被解析:

kotlin 复制代码
// A project that uses the example library for the JVM target

// MODULE: common
fun main() {
    whichFun(2)
    // platform function
}

相比之下, 如果你在同一源代码集中声明了whichFun()的重载, 通用代码中的函数将被解析, 因为你的代码无法访问特定于平台的版本:

kotlin 复制代码
// Example library isn't used

// MODULE: common
fun whichFun(x: Any) = println("common function")

fun main() {
    whichFun(2)
    // common function
}

// MODULE: JVM
fun whichFun(x: Int) = println("platform function")

与多平台库类似, 由于 commonTest 模块位于单独的源代码集中, 它仍然可以访问特定于平台的代码. 因此, commonTest模块中函数调用的解析与旧编译方案中的行为相同.

今后, 这些剩余情况将与新编译方案更加一致.

expect声明和actual声明的可见性级别不同

在 Kotlin 2.0.0 之前, 如果你在 Kotlin 多平台项目中使用expect声明和actual声明, 它们必须具有相同的可见性级别. Kotlin 2.0.0 现在也支持不同的可见性级别, 但前提 是actual声明比expect声明具有更大的许可性. 例如:

kotlin 复制代码
expect internal class Attribute // Visibility is internal
actual class Attribute          // Visibility is public by default,
                                // which is more permissive

同样, 如果你在actual声明中使用了类型别名, 那么基础类型的可见性应与expect声明相同或更宽松. 例如:

kotlin 复制代码
expect internal class Attribute                 // Visibility is internal
internal actual typealias Attribute = Expanded

class Expanded                                  // Visibility is public by default,
                                                // which is more permissive

编译器插件支持

目前, Kotlin K2 编译器支持以下 Kotlin 编译器插件:

此外, Kotlin K2 编译器还支持:

如果你使用任何其他编译器插件, 请查看它们的文档, 了解它们是否与 K2 兼容.

实验性的 Kotlin Power-assert 编译器插件

Kotlin Power-assert 插件是Experimental. 它可能随时更改.

Kotlin 2.0.0 引入了一个实验性 Power-assert 编译器插件. 该插件通过在失败消息中包含上下文信息来改善编写测试的体验, 从而使调试更简单, 更高效.

开发人员通常需要使用复杂的断言库来编写有效的测试. Power-assert插件能自动生成包含断言表达式中间值的失败消息, 从而简化了这一过程. 这有助于开发人员快速了解测试失败的原因.

当断言在测试中失败时, 改进后的错误消息会显示断言中所有变量和子表达式的值, 从而清楚地说明是哪部分条件导致了失败. 这对于需要检查多个条件的复杂断言尤其有用.

要在项目中启用该插件, 请在build.gradle(.kts)文件中进行配置:

Kotlin:

bash 复制代码
plugins {
    kotlin("multiplatform") version "2.0.0"
    kotlin("plugin.power-assert") version "2.0.0"
}

powerAssert {
    functions = listOf("kotlin.assert", "kotlin.test.assertTrue")
}

Groovy:

bash 复制代码
plugins { 
    id 'org.jetbrains.kotlin.multiplatform' version '2.0.0' 
    id 'org.jetbrains.kotlin.plugin.power-assert' version '2.0.0' 
} 
powerAssert { 
    functions = ["kotlin.assert", "kotlin.test.assertTrue"] 
}

了解更多有关Kotlin Power-assert 插件的信息.

如何启用 Kotlin K2 编译器

从 Kotlin 2.0.0 开始, 默认启用 Kotlin K2 编译器. 无需额外操作.

在 Kotlin Playground 中试用 Kotlin K2 编译器

Kotlin Playground 支持 2.0.0 版本. 快来看看吧!

IDE 中的支持

默认情况下, IntelliJ IDEA 和 Android Studio 仍使用以前的编译器进行代码分析, 代码补全, 高亮显示和其他 IDE 相关功能. 要在 IDE 中获得完整的 Kotlin 2.0 体验, 请启用 K2 Kotlin 模式.

在IDE中, 进入Settings | Languages & Frameworks | Kotlin , 然后选择Enable the K2-based Kotlin plugin选项. IDE将使用 K2 Kotlin 模式分析你的代码.

K2 Kotlin 模式处于 Alpha 阶段, 从 2024.1 开始可用. 代码高亮和代码自动补全的性能和稳定性已得到显著提高, 但目前还不支持IDE的所有功能.

启用 K2 模式后, 你可能会注意到 IDE 分析因编译器行为变化而产生的差异. 请参阅Jetbrains的 迁移指南, 了解新的 K2 编译器与之前的编译器有何不同.

  • Jetbrains的博客 中了解有关 K2 Kotlin 模式的更多信息.
  • Jetbrains正在积极收集有关 K2 Kotlin 模式的反馈意见. 请在Jetbrains的 public Slack channel 中分享你的想法.

留下你对新 K2 编译器的反馈意见

如果你有任何反馈, Jetbrains将不胜感激!

Kotlin/JVM

该版本带来了以下变更:

  • 使用 invokedynamic 生成 lambda 函数
  • kotlinx-metadata-jvm库现已稳定

使用 invokedynamic 生成 lambda 函数

Kotlin 2.0.0 引入了使用 invokedynamic 生成 lambda 函数的新默认方法. 与传统的匿名类生成方法相比, 这一改变减少了应用程序的二进制大小.

从第一个版本开始, Kotlin 就将 lambda 生成为匿名类. 不过, 从 Kotlin 1.5.0 开始, invokedynamic生成选项可通过使用-Xlambdas=indy编译器选项来实现. 在 Kotlin 2.0.0 中, invokedynamic已成为 lambda 生成的默认方法. 这种方法生成的二进制文件更轻量, 并使 Kotlin 与 JVM 优化保持一致, 从而确保应用程序受益于 JVM 性能的持续和未来改进.

目前, 与通用的 lambda 编译相比, 它有三个限制:

  • 编译为invokedynamic 的 lambda 是不可序列化的.
  • 试验性reflect() API 不支持由 invokedynamic 生成的 lambda.
  • 在这样的 lambda 上调用 .toString() 会产生可读性较差的字符串表示:
kotlin 复制代码
fun main() {
    println({})

    // With Kotlin 1.9.24 and reflection, returns
    // () -> kotlin.Unit

    // With Kotlin 2.0.0, returns
    // FileKt$$Lambda$13/0x00007f88a0004608@506e1b77
}

要保留生成 lambda 函数的传统行为, 可以采用以下两种方法:

  • 使用 @JvmSerializableLambda 对特定 lambda 进行注解.
  • 使用编译器选项 -Xlambdas=class 使用传统方法生成模块中的所有 lambda 函数.

kotlinx-metadata-jvm 库是稳定的

在 Kotlin 2.0.0 中, kotlinx-metadata-jvm 库已经稳定了. 现在, 该库已更改为 kotlin 包和坐标, 你可以以kotlin-metadata-jvm(去掉x)找到它.

以前, kotlinx-metadata-jvm 库有自己的发布方案和版本. 现在, Jetbrains将把 kotlin-metadata-jvm 更新作为 Kotlin 发布周期的一部分进行构建和发布, 并与 Kotlin 标准库一样保证向后兼容.

kotlin-metadata-jvm库提供了一个 API, 用于读取和修改由 Kotlin/JVM 编译器生成的二进制文件的元数据.

Kotlin/Native

该版本带来了以下变更:

  • 使用signpost监控 GC 性能
  • 解决与 Objective-C 方法的冲突
  • 更改了 Kotlin/Native 中编译器参数的日志级别
  • 为 Kotlin/Native 显式添加标准库和平台依赖
  • Gradle配置缓存中的任务错误

在苹果平台上使用signpost监控 GC 性能

以前, 只能通过查看日志来监控 Kotlin/Native 垃圾收集器(GC)的性能. 但是, 这些日志并没有与 Xcode Instruments 集成, 而 Xcode Instruments 是一种用于调查 iOS 应用程序性能问题的流行工具包.

从 Kotlin 2.0.0 开始, GC 会使用 Instruments 中的signpost报告暂停情况. Signpost允许在应用中自定义日志, 因此现在调试 iOS 应用性能时, 可以检查 GC 暂停是否与应用冻结相对应.

有关 GC 性能分析的更多信息, 请参阅这篇文档.

解决与 Objective-C 方法的冲突

Objective-C 方法可以有不同的名称, 但参数的数量和类型相同. 例如, locationManager:didEnterRegion:locationManager:didExitRegion:. 在 Kotlin 中, 这些方法具有相同的签名, 因此尝试使用它们会触发有冲突的重载错误.

以前, 你必须手动抑制冲突重载才能避免这种编译错误. 为了提高 Kotlin 与 Objective-C 的互操作性, Kotlin 2.0.0 引入了新的 @ObjCSignatureOverride 注解.

如果从 Objective-C 类继承了几个参数类型相同但参数名称不同的函数, 该注解会指示 Kotlin 编译器忽略冲突的重载.

应用该注解比一般的错误抑制更安全. 该注解只能在覆盖 Objective-C 方法的情况下使用, 因为 Objective-C 方法是经过支持和测试的, 而一般的错误抑制可能会隐藏重要的错误并导致代码被破坏.

更改了编译器参数的日志级别

在此版本中, Kotlin/Native Gradle 任务(如 compile, linkcinterop)中编译器参数的日志级别已从 info 变为 debug.

debug 作为默认值, 日志级别与其他 Gradle 编译任务一致, 并提供详细的调试信息, 包括所有编译器参数.

为 Kotlin/Native 显式添加标准库和平台依赖

Kotlin/Native 编译器曾以隐式方式解决标准库和平台依赖性问题, 这导致 Kotlin Gradle 插件在不同 Kotlin 目标之间的工作方式不一致.

现在, 每个 Kotlin/Native Gradle 编译都会通过 compileDependencyFiles 编译参数, 在编译时库路径中明确包含标准库和平台依赖.

Gradle配置缓存中的任务错误

自 Kotlin 2.0.0 起, 你可能会遇到配置缓存错误, 提示信息包括 在执行时调用 Task.project 是不支持的.

此错误出现在 NativeDistributionCommonizerTaskKotlinNativeCompile 等任务中.

不过, 这是一个假阳性错误. 根本问题是存在与 Gradle 配置缓存不兼容的任务, 如 publish* 任务.

这种差异可能不会立即显现, 因为错误信息暗示了另一个根本原因.

由于错误报告中没有明确指出确切原因, Gradle 团队已经在处理该问题以修复报告.

Kotlin/Wasm

Kotlin 2.0.0 提高了性能以及与 JavaScript 的互操作性:

  • 默认使用 Binaryen 优化生产构建
  • 支持命名导出
  • 使用@JsExport在函数中支持无符号基元类型
  • 在 Kotlin/Wasm 中生成 TypeScript 声明文件
  • 支持捕获 JavaScript 异常
  • 选项下支持新的异常处理建议
  • withWasm()函数分为 JS 和 WASI 变体

默认使用 Binaryen 优化生产构建

Kotlin/Wasm 工具链现在会在所有项目的生产编译过程中应用 Binaryen 工具, 而不是之前的手动设置方法. 根据Jetbrains的估计, 这将改善运行时性能和项目的二进制文件大小.

此变更仅影响生产编译. 开发编译流程保持不变.

支持命名导出

以前, 所有从 Kotlin/Wasm 导出的声明都使用默认导出方式导入 JavaScript:

csharp 复制代码
//JavaScript:
import Module from "./index.mjs"

Module.add()

现在, 你可以通过名称导入标有 @JsExport 的每个 Kotlin 声明:

kotlin 复制代码
// Kotlin:
@JsExport
fun add(a: Int, b: Int) = a + b
csharp 复制代码
//JavaScript:
import { add } from "./index.mjs"

命名导出使 Kotlin 和 JavaScript 模块之间的代码共享变得更容易. 它们提高了可读性, 并帮助你管理模块间的依赖关系.

使用 @JsExport 在函数中支持无符号基础类型

从 Kotlin 2.0.0 开始, 你可以通过 @JsExport 注解在外部声明和函数中使用 无符号基础数据类型, 从而在 JavaScript 代码中使用 Kotlin/Wasm 函数.

这有助于减轻以前阻止在导出和外部声明中直接使用 无符号基础数据类型的限制. 现在, 你可以导出将无符号基础数据作为返回或参数类型的函数, 也可以使用返回或使用无符号基础数据类型的外部声明.

有关 Kotlin/Wasm 与 JavaScript 互操作性的更多信息, 请参阅这篇文档.

在 Kotlin/Wasm 中生成 TypeScript 声明文件

在 Kotlin/Wasm 中生成 TypeScript 声明文件是 试验性的. 它可能随时被放弃或更改.

在 Kotlin 2.0.0 中, Kotlin/Wasm 编译器现在能够从 Kotlin 代码中的任何 @JsExport 声明生成 TypeScript 定义. IDE和 JavaScript 工具可以使用这些定义来提供代码自动完成功能, 帮助进行类型检查, 并使在 JavaScript 中包含 Kotlin 代码变得更容易.

Kotlin/Wasm 编译器会收集任何标有 @JsExport顶级函数 并自动在 .d.ts 文件中生成 TypeScript 定义.

要生成 TypeScript 定义, 请在build.gradle(.kts)文件的wasmJs {} 块中添加generateTypeScriptDefinitions()函数:

scss 复制代码
kotlin {
    wasmJs {
        binaries.executable()
        browser {
        }
        generateTypeScriptDefinitions()
    }
}

支持捕获 JavaScript 异常

以前, Kotlin/Wasm 代码无法捕获 JavaScript 异常, 因此很难处理来自程序 JavaScript 端的错误.

在 Kotlin 2.0.0 中, Jetbrains在 Kotlin/Wasm 中实现了对捕获 JavaScript 异常的支持. 这种实现允许你使用具有特定类型(如 ThrowableJsException )的 try-catch 块来正确处理这些错误.

此外, finally代码块也能正确工作, 它有助于执行代码而不管异常情况如何. 虽然Jetbrains正在引入对 JavaScript 异常捕获的支持, 但在 JavaScript 异常发生时, Jetbrains不会提供额外的信息, 如调用堆栈. 不过, Jetbrains正在努力实现这些功能.

新的异常处理建议现已在选项中得到支持

在本版本中, Jetbrains在 Kotlin/Wasm 中引入了对新版 WebAssembly 异常处理建议 的支持.

此更新可确保新建议与 Kotlin 要求保持一致, 从而使 Kotlin/Wasm 能够在仅支持最新版本建议的虚拟机上使用.

使用 -Xwasm-use-new-exception-proposal 编译器选项激活新的异常处理建议. 默认情况下该选项是关闭的.

withWasm() 函数分为 JS 和 WASI 变体

用于为层次结构模板提供 Wasm 目标的 withWasm() 函数已被弃用, 取而代之的是专门的 withWasmJs()withWasmWasi() 函数.

现在, 你可以在树定义中将 WASI 和 JS 目标分隔为不同的组.

Kotlin/JS

除其他更改外, 该版本还为 Kotlin 带来了现代 JS 编译, 支持 ES2015 标准的更多特性:

  • 新编译目标
  • 将函数暂停为 ES2015 生成器
  • 向主函数传递参数
  • 针对 Kotlin/JS 项目的每文件编译
  • 改进了集合互操作性
  • 支持 createInstance()
  • 支持类型安全的纯 JavaScript 对象
  • 支持 npm 软件包管理器
  • 编译任务变更
  • 停用遗留的 Kotlin/JS JAR 工件

新编译目标

在 Kotlin 2.0.0 中, Jetbrains为 Kotlin/JS 添加了一个新的编译目标 es2015. 这是一种新方法, 可让你一次性启用 Kotlin 支持的所有 ES2015 功能.

你可以在build.gradle(.kts)文件中这样设置:

c 复制代码
kotlin {
    js {
        compilerOptions {
            target.set("es2015")
        }
    }
}

新目标会自动打开 ES 类和模块 和新支持的 ES 生成器.

suspend函数作为 ES2015 生成器

本版本引入了对 ES2015 生成器的 Experimental 支持, 用于编译 suspend函数.

使用生成器而不是状态机可以改善项目的最终捆绑包大小. 例如, JetBrains 团队通过使用 ES2015 生成器, 将 Space 项目的捆绑包大小减少了 20%.

有关 ES2015(ECMAScript 2015, ES6)的更多信息, 请参阅官方文档.

向main函数传递参数

从 Kotlin 2.0.0 开始, 你可以为 main() 函数指定args来源. 此功能可让你更轻松地使用命令行并传递参数.

为此, 请使用新的passAsArgumentToMainFunction()函数定义js {}块, 该函数返回一个字符串数组:

scss 复制代码
kotlin {
    js {
        binary.executable()
        passAsArgumentToMainFunction("Deno.args")
    }
}

该函数在运行时执行. 它接收 JavaScript 表达式并将其用作args: Array<String>参数, 而不是调用 main() 函数.

此外, 如果使用 Node.js 运行时, 还可以利用一个特殊的别名. 它允许你将 process.argv 传递给 args 参数一次, 而不是每次都手动添加:

scss 复制代码
kotlin {
    js {
        binary.executable()
        nodejs {
            passProcessArgvToMainFunction()
        }
    }
}

Kotlin/JS 项目的按文件编译

Kotlin 2.0.0 为 Kotlin/JS 项目输出引入了一个新的粒度选项. 现在可以设置按文件编译, 每个 Kotlin 文件生成一个 JavaScript 文件. 这有助于大幅优化最终捆绑包的大小, 并改善程序的加载时间.

以前, 只有两种输出选项. Kotlin/JS 编译器可以为整个项目生成一个 .js 文件. 但是, 这个文件可能太大, 使用起来不方便. 每当要使用项目中的某个函数时, 就必须将整个 JavaScript 文件作为依赖项. 或者, 也可以为每个项目模块配置编译一个单独的 .js 文件. 这仍然是默认选项.

由于模块文件也可能过大, 因此在 Kotlin 2.0.0 中, Jetbrains添加了更细粒度的输出, 为每个 Kotlin 文件生成一个(或两个, 如果文件包含导出声明)JavaScript 文件. 要启用按文件编译模式, 请执行以下操作:

  1. 在构建文件中添加 useEsModules() 函数, 以支持 ECMAScript 模块:

    scss 复制代码
    // build.gradle.kts
    kotlin {
        js(IR) {
            useEsModules() // Enables ES2015 modules
            browser()
        }
    }

    你也可以使用新的es2015编译目标来实现这一点.

  2. 应用 -Xir-per-file 编译器选项, 或更新 gradle.properties 文件:

    sql 复制代码
    # gradle.properties
    kotlin.js.ir.output.granularity=per-file // `per-module` is the default

改进了的集合互操作性

从 Kotlin 2.0.0 开始, 可以将签名中包含 Kotlin 集合类型的声明导出到 JavaScript(和 TypeScript). 这适用于 Set, MapList 集合类型及其可变对应类型.

要在 JavaScript 中使用 Kotlin 集合, 首先要用 @JsExport 注解标记必要的声明:

less 复制代码
// Kotlin
@JsExport
data class User(
    val name: String,
    val friends: List<User> = emptyList()
)

@JsExport
val me = User(
    name = "Me",
    friends = listOf(User(name = "Kodee"))
)

然后, 你就可以像使用通用 JavaScript 数组一样从 JavaScript 中使用它们:

javascript 复制代码
// JavaScript
import { User, me, KtList } from "my-module"

const allMyFriendNames = me.friends
    .asJsReadonlyArrayView()
    .map(x => x.name) // ['Kodee']

遗憾的是, 从 JavaScript 创建 Kotlin 集合的功能仍然不可用. Jetbrains计划在 Kotlin 2.0.20 中添加此功能.

支持 createInstance()

自 Kotlin 2.0.0 起, 你可以在 Kotlin/JS 目标中使用 createInstance() 函数. 以前, 它只能在 JVM 上使用.

这个来自 KClass 接口的函数会创建一个指定类的新实例, 对于获取 Kotlin 类的运行时引用非常有用.

支持类型安全的纯 JavaScript 对象

js-plain-objects 插件是试验性的. 它可能随时被放弃或更改. js-plain-objects插件支持 K2 编译器.

为了更方便地使用 JavaScript API, Jetbrains在 Kotlin 2.0.0 中提供了一个新插件: js-plain-objects, 你可以用它来创建类型安全的纯 JavaScript 对象. 该插件会检查你的代码中是否有任何带有 @JsPlainObject 注解的 外部接口, 并添加:

  • 可用作构造函数的同伴对象内部的内联 invoke 运算符函数.
  • 一个.copy()函数, 可以用来创建对象的副本, 同时调整其某些属性.

例如:

kotlin 复制代码
import kotlinx.js.JsPlainObject

@JsPlainObject
external interface User {
    var name: String
    val age: Int
    val email: String?
}

fun main() {
    // Creates a JavaScript object
    val user = User(name = "Name", age = 10)
    // Copies the object and adds an email
    val copy = user.copy(age = 11, email = "some@user.com")

    println(JSON.stringify(user))
    // { "name": "Name", "age": 10 }
    println(JSON.stringify(copy))
    // { "name": "Name", "age": 11, "email": "some@user.com" }
}

使用这种方法创建的任何 JavaScript 对象都更安全, 因为你可以在编译时看到错误, 甚至在IDE中高亮显示错误, 而不是在运行时才看到错误.

请看这个示例, 它使用一个 fetch() 函数与 JavaScript API 进行交互, 使用外部接口来描述 JavaScript 对象的形状:

kotlin 复制代码
import kotlinx.js.JsPlainObject

@JsPlainObject
external interface FetchOptions {
    val body: String?
    val method: String
}

// A wrapper for Window.fetch
suspend fun fetch(url: String, options: FetchOptions? = null) = TODO("Add your custom behavior here")

// A compile-time error is triggered as "metod" is not recognized
// as method
fetch("https://google.com", options = FetchOptions(metod = "POST"))
// A compile-time error is triggered as method is required
fetch("https://google.com", options = FetchOptions(body = "SOME STRING"))

相比之下, 如果你使用 js() 函数来创建 JavaScript 对象, 错误只会在运行时发现, 或者根本不会触发:

kotlin 复制代码
suspend fun fetch(url: String, options: FetchOptions? = null) = TODO("Add your custom behavior here")

// No error is triggered. As "metod" is not recognized, the wrong method
// (GET) is used.
fetch("https://google.com", options = js("{ metod: 'POST' }"))

// By default, the GET method is used. A runtime error is triggered as
// body shouldn't be present.
fetch("https://google.com", options = js("{ body: 'SOME STRING' }"))
// TypeError: Window.fetch: HEAD or GET Request cannot have a body

要使用js-plain-objects插件, 请在build.gradle(.kts)文件中添加以下内容:

Kotlin:

arduino 复制代码
plugins {
    kotlin("plugin.js-plain-objects") version "2.0.0"
}

Groovy:

bash 复制代码
plugins { 
    id "org.jetbrains.kotlin.plugin.js-plain-objects" version "2.0.0" 
}

支持 npm 软件包管理器

以前, Kotlin 多平台 Gradle 插件只能使用 Yarn 作为包管理器来下载和安装 npm 依赖项. 从 Kotlin 2.0.0 开始, 你可以改用 npm 作为包管理器. 使用 npm 作为软件包管理器意味着在安装过程中少了一个需要管理的工具.

为了向后兼容, Yarn 仍是默认的软件包管理器. 要使用 npm 作为软件包管理器, 请在gradle.properties文件中设置以下属性:

ini 复制代码
kotlin.js.yarn = false

编译任务变更

以前, webpackdistributeResources 编译任务的目标都是相同的目录. 此外, distribution 任务还将 dist 声明为其输出目录. 这导致了输出重叠, 并产生了编译警告.

因此, 从 Kotlin 2.0.0 开始, Jetbrains进行了以下更改:

  • webpack 任务现在以单独的文件夹为目标.
  • 完全删除了 distributeResources 任务.
  • distribution任务现在具有 Copy类型, 并以 dist "文件夹为目标.

停止使用遗留的 Kotlin/JS JAR 工件

从 Kotlin 2.0.0 开始, Kotlin 发行版不再包含扩展名为 .jar 的传统 Kotlin/JS 工件. 旧版工件被用于不支持的旧版 Kotlin/JS 编译器, 对于使用 klib 格式的 IR 编译器来说则没有必要.

Gradle 的改进

Kotlin 2.0.0 与 Gradle 6.8.3 至 8.5 完全兼容. 你也可以使用最新发布的 Gradle 版本, 但请注意, 你可能会遇到弃用警告, 或者一些新的 Gradle 功能可能无法使用.

该版本带来了以下变化::

  • 用于多平台项目中编译器选项的新 Gradle DSL
  • 新的 Compose 编译器 Gradle 插件
  • 区分 JVM 和 Android 发布的库的新属性
  • 改进了 Kotlin/Native 中 CInteropProcess 的 Gradle 依赖处理
  • Gradle 中的可见性变化
  • Gradle 项目中 Kotlin 数据的新目录
  • 需要时下载 Kotlin/Native 编译器
  • 淘汰旧的编译器选项定义方法
  • 提升最低 AGP 支持版本
  • 尝试最新语言版本的新 Gradle 属性
  • 新的构建报告 JSON 输出格式
  • kapt 配置从超级配置继承注解处理器
  • Kotlin Gradle 插件不再使用过时的 Gradle 约定

新的 Gradle DSL, 用于多平台项目中的编译器选项

此功能为实验性. 随时可能被删除或更改. 仅用于评估目的. Jetbrains希望你能在 YouTrack 上提供反馈意见.

在 Kotlin 2.0.0 之前, 使用 Gradle 在多平台项目中配置编译器选项只能在较低层次上进行, 如按任务, 编译或源集来配置. 为了更方便地在项目中配置编译器选项, Kotlin 2.0.0 附带了一个新的 Gradle DSL.

使用这个新的 DSL, 你可以在扩展级别为所有目标和共享源码集(如 commonMain)配置编译器选项, 也可以在目标级别为特定目标配置编译器选项:

c 复制代码
kotlin {
    compilerOptions {
        // Extension-level common compiler options that are used as defaults
        // for all targets and shared source sets
        allWarningsAsErrors.set(true)
    }
    jvm {
        compilerOptions {
            // Target-level JVM compiler options that are used as defaults
            // for all compilations in this target
            noJdk.set(true)
        }
    }
}

现在, 整个项目配置有三个层次. 最高层是扩展层, 然后是目标层, 最低层是编译单元(通常是编译任务):

Kotlin compiler options levels

较高层次的设置被用作较低层次的惯例(默认值):

  • 扩展编译器选项的值是目标编译器选项的默认值, 包括共享源代码集, 如 commonMain, nativeMaincommonTest.
  • 目标编译器选项的值被用作编译单元(任务)编译器选项的默认值, 例如 compileKotlinJvmcompileTestKotlinJvm 任务.

反过来, 较低级别的配置会覆盖较高级别的相关设置:

  • 任务级编译器选项覆盖目标或扩展级的相关配置.
  • 目标层编译器选项覆盖扩展层的相关配置.

在配置项目时, 请记住一些旧的编译器选项设置方式已经废弃.

Jetbrains鼓励大家在多平台项目中试用新的 DSL, 并在 YouTrack 上留下反馈意见, 因为Jetbrains计划将此 DSL 作为配置编译器选项的推荐方法.

新的 Compose 编译器 Gradle 插件

Jetpack Compose 编译器可将可编译代码转换为 Kotlin 代码, 现已并入 Kotlin 代码库. 这将有助于将 Compose 项目过渡到 Kotlin 2.0.0, 因为 Compose 编译器将始终与 Kotlin 同步发布. 这也会将金豪编译器版本提升到 2.0.0.

要在项目中使用新的 Compose 编译器, 请在build.gradle(.kts)文件中应用org.jetbrains.kotlin.plugin.compose Gradle 插件, 并将其版本设为 Kotlin 2.0.0.

要了解有关此更改的更多信息并查看迁移说明, 请参阅 Compose 编译器 文档.

区分 JVM 和 Android 发布的库的新属性

从 Kotlin 2.0.0 开始, org.gradle.jvm.environment Gradle 属性默认与所有 Kotlin 变体一起发布.

该属性有助于区分 Kotlin 多平台库的 JVM 和 Android 变体. 它表明某个库变体更适合某个 JVM 环境. 目标环境可以是android, standard-jvmno-jvm.

发布此属性可使非多平台客户端(如纯 Java 项目)在使用带有 JVM 和 Android 目标环境的 Kotlin 多平台库时更加稳健.

如有必要, 你可以禁用属性发布. 为此, 请在gradle.properties文件中添加以下 Gradle 选项:

ini 复制代码
kotlin.publishJvmEnvironmentAttribute=false

改进了 Kotlin/Native 中 CInteropProcess 的 Gradle 依赖处理

在此版本中, Jetbrains增强了对 defFile 属性的处理, 以确保在 Kotlin/Native 项目中更好地管理 Gradle 任务的依赖关系.

在本次更新之前, 如果 defFile 属性被指定为另一个尚未执行的任务的输出, Gradle 编译可能会失败. 该问题的解决方法是在该任务上添加依赖:

scss 复制代码
kotlin {
    macosArm64("native") {
        compilations.getByName("main") {
            cinterops {
                val cinterop by creating {
                    defFileProperty.set(createDefFileTask.flatMap { it.defFile.asFile })
                    project.tasks.named(interopProcessingTaskName).configure {
                        dependsOn(createDefFileTask)
                    }
                }
            }
        }
    }
}

为了解决这个问题, Jetbrains新增了一个名为 definitionFileRegularFileProperty 属性. 现在, Gradle 会在连接的任务运行之后, 在构建过程中懒散地验证 definitionFile 属性是否存在. 这种新方法无需额外的依赖关系.

CInteropProcess任务和CInteropSettings类使用 definitionFile属性, 而不是 defFiledefFileProperty:

Kotlin:

javascript 复制代码
kotlin {
    macosArm64("native") {
        compilations.getByName("main") {
            cinterops {
                val cinterop by creating {
                    definitionFile.set(project.file("def-file.def"))
                }
            }
        }
    }
}

Groovy:

javascript 复制代码
kotlin { 
    macosArm64("native") { 
        compilations.main { 
            cinterops { 
                cinterop { 
                    definitionFile.set(project.file("def-file.def")) 
                } 
            } 
        } 
    } 
}

defFiledefFileProperty参数已废弃.

Gradle 中的可见性变化

此更改仅影响 Kotlin DSL 用户.

在 Kotlin 2.0.0 中, Jetbrains修改了 Kotlin Gradle 插件, 以便在构建脚本中实现更好的控制和安全性. 以前, 某些针对特定 DSL 上下文的 Kotlin DSL 函数和属性会无意中泄漏到其他 DSL 上下文中. 这种泄漏可能会导致使用不正确的编译器选项, 设置被多次应用以及其他错误配置:

scss 复制代码
kotlin {
    // Target DSL couldn't access methods and properties defined in the
    // kotlin{} extension DSL
    jvm {
        // Compilation DSL couldn't access methods and properties defined
        // in the kotlin{} extension DSL and Kotlin jvm{} target DSL
        compilations.configureEach {
            // Compilation task DSLs couldn't access methods and
            // properties defined in the kotlin{} extension, Kotlin jvm{}
            // target or Kotlin compilation DSL
            compileTaskProvider.configure {
                // For example:
                explicitApi()
                // ERROR as it is defined in the kotlin{} extension DSL
                mavenPublication {}
                // ERROR as it is defined in the Kotlin jvm{} target DSL
                defaultSourceSet {}
                // ERROR as it is defined in the Kotlin compilation DSL
            }
        }
    }
}

为了解决这个问题, Jetbrains添加了 @KotlinGradlePluginDsl 注解, 防止 Kotlin Gradle 插件的 DSL 函数和属性泄露到不打算提供这些函数和属性的层级. 以下级别相互分离:

  • Kotlin 扩展
  • Kotlin 目标
  • Kotlin 编译
  • Kotlin 编译任务

对于最常见的情况, Jetbrains添加了编译器警告, 并建议你在编译脚本配置错误时如何修复. 例如:

scss 复制代码
kotlin {
    jvm {
        sourceSets.getByName("jvmMain").dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3")
        }
    }
}

在这种情况下, sourceSets的警告信息是:

csharp 复制代码
[DEPRECATION] 'sourceSets: NamedDomainObjectContainer<KotlinSourceSet>' is deprecated.Accessing 'sourceSets' container on the Kotlin target level DSL is deprecated. Consider configuring 'sourceSets' on the Kotlin extension level.

Jetbrains希望你能对此更改提出反馈意见! 请在Jetbrains的 #gradle Slack 频道 中直接向 Kotlin 开发人员分享你的意见. 获取 Slack 邀请.

Gradle 项目中 Kotlin 数据的新目录

有了这个变化, 你可能需要在项目的 .gitignore 文件中添加 .kotlin 目录.

在 Kotlin 1.8.20 中, Kotlin Gradle 插件转而将数据存储在 Gradle 项目缓存目录中:<project-root-directory>/.gradle/kotlin. 但是, .gradle 目录只为 Gradle 保留, 因此无法面向未来.

为了解决这个问题, 从 Kotlin 2.0.0 开始, Jetbrains将默认把 Kotlin 数据存储在 <项目根目录>/.kotlin中. 为了向后兼容, Jetbrains将继续在.gradle/kotlin目录下存储一些数据.

你可以配置的新 Gradle 属性有

Gradle 属性 说明
kotlin.project.persistent.dir 配置项目级数据的存储位置. 默认值:<project-root-directory>/.kotlin
kotlin.project.persistent.dir 配置项目级数据的存储位置.
kotlin.project.persistent.dir.gradle.disableWrite 一个布尔值, 用于控制是否禁止将 Kotlin 数据写入.gradle目录. 默认值:"false

将这些属性添加到项目的 gradle.properties 文件中, 使其生效.

需要时下载 Kotlin/Native 编译器

在 Kotlin 2.0.0 之前, 如果在多平台项目的 Gradle 编译脚本中配置了Kotlin/Native 目标, Gradle 总是会在配置阶段下载 Kotlin/Native 编译器.

即使在执行阶段没有任务要为Kotlin/Native目标编译代码, 这种情况也会发生. 对于只想检查项目中的 JVM 或 JavaScript 代码的用户来说, 以这种方式下载 Kotlin/Native 编译器的效率尤其低下. 例如, 作为 CI 流程的一部分, 对其 Kotlin 项目执行测试或检查.

在 Kotlin 2.0.0 中, Jetbrains在 Kotlin Gradle 插件中改变了这一行为, 使 Kotlin/Native 编译器在执行阶段下载, 并且在请求编译 Kotlin/Native 目标时下载.

反过来, Kotlin/Native 编译器的依赖项现在不是作为编译器的一部分下载, 而是在执行阶段下载.

如果在使用新行为时遇到任何问题, 可以通过在gradle.properties文件中添加以下 Gradle 属性, 暂时切换回以前的行为:

ini 复制代码
kotlin.native.toolchain.enabled=false

从 1.9.20-Beta 版开始, Kotlin/Native 发行版与 CDN 一起发布到 Maven Central.

这让Jetbrains得以改变 Kotlin 查找和下载必要工件的方式. 现在, 它默认使用在项目的 repositories {} 块中指定的 Maven 资源库, 而不是 CDN.

你可以通过在 gradle.properties 文件中设置以下 Gradle 属性, 暂时将此行为切换回来:

ini 复制代码
kotlin.native.distribution.downloadFromMaven=false.

如有任何问题, 请向Jetbrains的问题跟踪器 YouTrack 报告. 这两个改变默认行为的 Gradle 属性都是临时的, 将在以后的版本中移除.

过时的编译器选项定义方法

在此版本中, Jetbrains将继续改进编译器选项的设置方式. 这将解决不同方式之间的歧义, 并使项目配置更加简单明了.

自 Kotlin 2.0.0 起, 以下用于指定编译器选项的 DSL 已被弃用:

  • 实现所有 Kotlin 编译任务的 KotlinCompile 接口中的 kotlinOptions DSL. 请使用 KotlinCompilationTask<CompilerOptions> 代替.

  • KotlinCompiation 接口的 compilerOptions 属性与 HasCompilerOptions 类型. 该 DSL 与其他 DSL 不一致, 它将同一个 KotlinCommonCompilerOptions 对象配置为 KotlinCompilation.compileTaskProvider 编译任务中的 compilerOptions 对象, 这会造成混淆.

    Jetbrains建议使用 Kotlin 编译任务中的 compilerOptions 属性:

    markdown 复制代码
    kotlinCompilation.compileTaskProvider.configure {
        compilerOptions { ... }
    }

    例如:

    javascript 复制代码
    kotlin {
        js(IR) {
            compilations.all {
                compileTaskProvider.configure {
                    compilerOptions.freeCompilerArgs.add("-Xerror-tolerance-policy=SYNTAX")
                }
            }
        }
    }
  • KotlinCompilation 接口中的 kotlinOptions DSL.

  • KotlinNativeArtifactConfig接口, KotlinNativeLink类和KotlinNativeLinkArtifactTask类中获取kotlinOptionsDSL. 请使用 toolOptions DSL.

  • 来自 KotlinJsDce 接口的 dceOptions DSL. 请使用 toolOptions DSL.

有关如何在 Kotlin Gradle 插件中指定编译器选项的更多信息, 请参阅如何定义选项.

升级最小 AGP 版本

从 Kotlin 2.0.0 开始, Android Gradle 插件的最低支持版本为 7.1.3.

新增 Gradle 属性以尝试最新语言版本

在 Kotlin 2.0.0 之前, Jetbrains使用以下 Gradle 属性来试用新的 K2 编译器: kotlin.experimental.tryK2. 现在 Kotlin 2.0.0 默认启用了 K2 编译器, 因此Jetbrains决定将该属性演化成一种新形式, 你可以用它在项目中试用最新的语言版本: kotlin.experimental.tryNext. 在gradle.properties文件中使用该属性时, Kotlin Gradle 插件会将语言版本递增到比 Kotlin 版本默认值高一个的位置. 例如, 在 Kotlin 2.0.0 中, 默认语言版本是 2.0, 因此该属性配置的语言版本是 2.1.

这个新的 Gradle 属性在 构建报告中产生的指标与之前的 kotlin.experimental.tryK2相似. 输出中包含配置的语言版本. 例如:

ruby 复制代码
##### 'kotlin.experimental.tryNext' results #####
:app:compileKotlin: 2.1 language version
:lib:compileKotlin: 2.1 language version
##### 100% (2/2) tasks have been compiled with Kotlin 2.1 #####

要进一步了解如何启用构建报告及其内容, 请参阅 构建报告.

新的构建报告 JSON 输出格式

在 Kotlin 1.7.0 中, Jetbrains引入了构建报告来帮助跟踪编译器性能. 随着时间的推移, Jetbrains添加了更多指标, 使这些报告更加详细, 有助于调查性能问题. 以前, 本地文件的唯一输出格式是 *.txt 格式. 在 Kotlin 2.0.0 中, Jetbrains支持 JSON 输出格式, 使其更易于使用其他工具进行分析.

要为构建报告配置 JSON 输出格式, 请在 gradle.properties 文件中声明以下属性:

ini 复制代码
kotlin.build.report.output=json

// The directory to store your build reports
kotlin.build.report.json.directory="my/directory/path"

或者, 也可以运行以下命令:

ini 复制代码
./gradlew assemble -Pkotlin.build.report.output=json -Pkotlin.build.report.json.directory="my/directory/path"

配置完成后, Gradle 会在你指定的目录下生成构建报告, 并命名为:${project_name}-date-time-<sequence_number>.json.

下面是一个使用 JSON 输出格式的构建报告示例片段, 其中包含构建指标和汇总指标:

json 复制代码
"buildOperationRecord": [
    {
     "path": ":lib:compileKotlin",
      "classFqName": "org.jetbrains.kotlin.gradle.tasks.KotlinCompile_Decorated",
      "startTimeMs": 1714730820601,
      "totalTimeMs": 2724,
      "buildMetrics": {
        "buildTimes": {
          "buildTimesNs": {
            "CLEAR_OUTPUT": 713417,
            "SHRINK_AND_SAVE_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION": 19699333,
            "IR_TRANSLATION": 281000000,
            "NON_INCREMENTAL_LOAD_CURRENT_CLASSPATH_SNAPSHOT": 14088042,
            "CALCULATE_OUTPUT_SIZE": 1301500,
            "GRADLE_TASK": 2724000000,
            "COMPILER_INITIALIZATION": 263000000,
            "IR_GENERATION": 74000000,
...
          }
        }
...
 "aggregatedMetrics": {
    "buildTimes": {
      "buildTimesNs": {
        "CLEAR_OUTPUT": 782667,
        "SHRINK_AND_SAVE_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION": 22031833,
        "IR_TRANSLATION": 333000000,
        "NON_INCREMENTAL_LOAD_CURRENT_CLASSPATH_SNAPSHOT": 14890292,
        "CALCULATE_OUTPUT_SIZE": 2370750,
        "GRADLE_TASK": 3234000000,
        "COMPILER_INITIALIZATION": 292000000,
        "IR_GENERATION": 89000000,
...
      }
    }

kapt 配置从超配置继承注解处理器

在 Kotlin 2.0.0 之前, 如果你想在单独的 Gradle 配置中定义一套通用的注释处理器, 并在针对子项目的 kapt 配置中扩展该配置, kapt 会因为找不到注释处理器而跳过注释处理. 在 Kotlin 2.0.0 中, kapt 可以成功检测到注释处理器的间接依赖关系.

例如, 对于使用 Dagger的子项目, 在build.gradle(.kts)文件中使用以下配置:

scss 复制代码
val commonAnnotationProcessors by configurations.creating
configurations.named("kapt") { extendsFrom(commonAnnotationProcessors) }

dependencies {
    implementation("com.google.dagger:dagger:2.48.1")
    commonAnnotationProcessors("com.google.dagger:dagger-compiler:2.48.1")
}

在本例中,commonAnnotationProcessorsGradle 配置是用于注释处理的common配置, 你希望所有项目都使用它. 你使用 extendsFrom() 方法将commonAnnotationProcessors添加为超级配置. kapt 发现commonAnnotationProcessorsGradle 配置依赖于 Dagger 注释处理器, 并成功将其包含在注释处理配置中.

感谢 Christoph Loy 的 实现!

Kotlin Gradle 插件不再使用过时的 Gradle 约定

在 Kotlin 2.0.0 之前, 如果使用 Gradle 8.2 或更高版本, Kotlin Gradle 插件会错误地使用 Gradle 8.2 中已废弃的 Gradle 约定. 这导致 Gradle 报告了构建中的弃用. 在 Kotlin 2.0.0 中, Kotlin Gradle 插件已更新为在使用 Gradle 8.2 或更高版本时不再触发这些弃用警告.

标准库

此版本进一步提高了 Kotlin 标准库的稳定性, 并使更多现有函数适用于所有平台::

  • 稳定的枚举类值通用函数替代
  • 稳定的 AutoCloseable 接口
  • 通用的受保护属性 AbstractMutableList.modCount
  • 通用的受保护函数 AbstractMutableList.removeRange
  • 通用的String.toCharArray(destination)

稳定的枚举类值通用函数替代

在 Kotlin 2.0.0 中, enumEntries<T>() 函数已经稳定了. enumEntries<T>()函数是通用函数 enumValues<T>() 的替代函数. 新函数返回给定枚举类型 T 的所有枚举条目列表. 枚举类的 entries 属性是以前引入的, 现在也稳定下来, 以取代合成的 values() 函数. 有关 entries 属性的更多信息, 请参阅 Kotlin 1.8.20 的新特性.

仍然支持 enumValues<T>() 函数, 但Jetbrains建议你使用 enumEntries<T>() 函数, 因为它对性能的影响较小. 每次调用 enumValues<T>() 都会创建一个新数组, 而每次调用 enumEntries<T>() 都会返回相同的列表, 因此效率要高得多.

比如:

kotlin 复制代码
enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
    print(enumEntries<T>().joinToString { it.name })
}

printAllValues<RGB>()
// RED, GREEN, BLUE

稳定的 AutoCloseable 接口

在 Kotlin 2.0.0 中, 常用的 AutoCloseable 接口已经 稳定. 它允许你轻松关闭资源, 并包含几个有用的函数:

  • use() 扩展函数会在选定的资源上执行给定的块函数, 然后无论是否抛出异常, 都会正确地关闭资源.
  • AutoCloseable()构造函数, 用于创建 AutoCloseable接口的实例.

在下面的示例中, Jetbrains定义了 XMLWriter 接口, 并假设有一个资源实现了该接口. 例如, 该资源可以是一个打开文件, 写入 XML 内容然后关闭文件的类:

kotlin 复制代码
interface XMLWriter {
    fun document(encoding: String, version: String, content: XMLWriter.() -> Unit)
    fun element(name: String, content: XMLWriter.() -> Unit)
    fun attribute(name: String, value: String)
    fun text(value: String)

    fun flushAndClose()
}

fun writeBooksTo(writer: XMLWriter) {
    val autoCloseable = AutoCloseable { writer.flushAndClose() }
    autoCloseable.use {
        writer.document(encoding = "UTF-8", version = "1.0") {
            element("bookstore") {
                element("book") {
                    attribute("category", "fiction")
                    element("title") { text("Harry Potter and the Prisoner of Azkaban") }
                    element("author") { text("J. K. Rowling") }
                    element("year") { text("1999") }
                    element("price") { text("29.99") }
                }
                element("book") {
                    attribute("category", "programming")
                    element("title") { text("Kotlin in Action") }
                    element("author") { text("Dmitry Jemerov") }
                    element("author") { text("Svetlana Isakova") }
                    element("year") { text("2017") }
                    element("price") { text("25.19") }
                }
            }
        }
    }
}

通用的受保护属性 AbstractMutableList.modCount

在此版本中, AbstractMutableList接口的modCount protected属性变得通用. 以前, modCount 属性在每个平台上都可用, 但在通用目标上却不可用. 现在, 你可以创建 AbstractMutableList 的自定义实现, 并在通用代码中访问该属性.

该属性记录了对集合所做结构修改的次数. 这包括改变集合大小或以可能导致正在进行的迭代返回错误结果的方式改变列表的操作.

在实现自定义列表时, 可以使用 modCount 属性注册和检测并发修改.

通用的受保护函数 AbstractMutableList.removeRange

在此版本中, AbstractMutableList接口的removeRange()protected函数变得通用. 以前, 它在每个平台上都可用, 但在通用目标上却不可用. 现在, 你可以创建 AbstractMutableList 的自定义实现, 并在通用代码中覆盖该函数.

该函数将按照指定范围从列表中删除元素. 通过重载该函数, 你可以利用自定义实现并提高列表操作的性能.

通用的 String.toCharArray(destination) 函数

此版本引入了一个常用的 String.toCharArray(destination) 函数. 此前, 该函数仅在 JVM 上可用.

让Jetbrains将它与现有的 String.toCharArray() 函数进行比较. 它创建了一个新的 CharArray , 其中包含指定字符串中的字符. 然而, 新的通用 String.toCharArray(destination) 函数会将 String 字符移动到现有的目标 CharArray 中. 如果你已经有一个要填充的缓冲区, 这个函数就很有用:

kotlin 复制代码
fun main() {

    val myString = "Kotlin is awesome!"

    val destinationArray = CharArray(myString.length)

    // Convert the string and store it in the destinationArray:

    myString.toCharArray(destinationArray)

    for (char in destinationArray) {
        print("$char ")
        // K o t l i n   i s   a w e s o m e ! 
    }
}

安装 Kotlin 2.0.0

从 IntelliJ IDEA 2023.3 和 Android Studio Iguana (2023.2.1) Canary 15 开始, Kotlin 插件作为捆绑插件发布, 包含在你的 IDE 中. 这意味着你不能再从 JetBrains Marketplace 安装该插件.

要更新到新的 Kotlin 版本, 请在构建脚本中将 Kotlin 版本 更改为 2.0.0.

相关推荐
小比卡丘2 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭2 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss4 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.4 小时前
数据库语句优化
android·数据库·adb
一丝晨光7 小时前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
GEEKVIP7 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20058 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6899 小时前
Android广播
android·java·开发语言
与衫10 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了16 小时前
Kotlin基本知识
android·开发语言·kotlin