Kotlin 1.4-M1 现已发布!

发布于 JetBrains_CN

我们高兴地宣布新的主要版本的第一个预览版本:Kotlin 1.4-M1。

几个月以前,我们针对 Kotlin 1.4 值得期待的亮点发布了一则公告。随着发布临近,我们现在提供一个预览版本,让大家能够试用一些新功能。

在这篇博文中,我们将重点介绍 1.4-M1 中的以下新功能和主要改进:

默认启用一种功能更加强大的新类型推理算法

协定现在可用于 final 成员函数。

  • Kotlin/JVM 编译器现在可在字节码中为 Java 8 和更高版本生成类型注解。

  • Kotlin/JS 的新后端为生成的工件带来大幅提升。

  • 标准库中的渐进式变更:完成弃用周期并弃用一些额外的部分。

您可以在变更日志中找到完整的变更列表。我们要一如既往地感谢外部贡献者

我们强烈建议您试用预览版,并感谢您在我们的问题跟踪器中提供任何反馈。

功能更加强大的类型推理算法

Kotlin 1.4 将使用一种功能更加强大的新类型推理算法。您之前已经可以通过指定编译器选项在 Kotlin 1.3 中试用这种新算法,现在则可以默认使用。您可以在 YouTrack 中找到新算法中修复的完整问题列表。在这篇博文中,我们将重点介绍一些最值得注意的改进。

Kotlin 函数和接口的 SAM 转换

SAM 转换让您可以在预期出现一个包含“单抽象方法”的接口时传递 lambda。之前,您只能在 Kotlin 中使用 Java 方法和 Java 接口时应用 SAM 转换,现在您也可以将它与 Kotlin 函数和接口一起使用。

Kotlin 现在支持 Kotlin 接口的 SAM 转换。请注意,在 Java 中的操作不同:您需要明确地标记函数接口。使用 fun 关键字标记接口后,在接口预计为参数时,您都可以将 lambda 作为参数传递:

fun interface Action {
fun run()
}

fun runAction(a: Action) = a.run()

fun main() {
runAction {
println("Hello, Kotlin 1.4!")
}
}

您可以在之前的博文中阅读与此相关的更多详细信息。

Kotlin 从最开始就支持 Java 接口的 SAM 转换,但有一种情况不受支持,在使用现有 Java 库时会令人很烦。如果您调用了将两个 SAM 接口作为参数的 Java 方法,两个参数需要都为 lambda 或常规对象。不可以将一个参数作为 lambda 而另一个参数作为对象来传递。新算法修复了这个问题,在任何情况下您都可以传递 lambda,而不是 SAM 接口,这正是您期待的运作方式。

在更多用例中自动推断类型

新推理算法会为许多用例推断类型,而旧推理要求您明确地指定它们。例如,在以下示例中,lambda 参数 it 的类型将正确推断为 String?

val rulesMap: Map<String, (String?) -> Boolean> = mapOf(
    "weak" to { it != null },
    "medium" to { !it.isNullOrBlank() },
    "strong" to { it != null && "^[a-zA-Z0-9]+$".toRegex().matches(it) }
)

fun main() {
    println(rulesMap.getValue("weak")("abc!"))
    println(rulesMap.getValue("strong")("abc"))
    println(rulesMap.getValue("strong")("abc!"))
}

在 Kotlin 1.3 中,您需要引入显式 lambda 参数,或者将 to 替换为包含显式泛型参数的 Pair 构造函数才可以。

lambda 中最后一个表达式的智能转换

在 Kotlin 1.3 中,lambda 中的最后一个表达式不是智能转换,除非您指定预期的类型。因此,在以下示例中,Kotlin 1.3 将 String? 推断为 result 变量的类型:

val result = run {
    var str = currentValue()
    if (str == null) {
        str = "test"
    }
    str // the Kotlin compiler knows that str is not null here
}
// The type of 'result' is String? in Kotlin 1.3 and String in Kotlin 1.4

在 Kotlin 1.4 中,借助新的推理算法,lambda 中的最后一个表达式可以实现智能转换,并且这种更精确的新类型用于推断结果 lambda 类型。因此,result 变量的类型变为 String

在 Kotlin 1.3 中,您经常需要添加显式转换(!! 或类型转换,例如 as String)来使这种情况奏效,而现在这些转换不再必要。

可调用的引用的智能转换

在 Kotlin 1.3 中,您无法访问智能转换类型的成员引用。现在,您可以:

sealed class Animal
class Cat : Animal() {
    fun meow() {
        println("meow")
    }
}

class Dog : Animal() {
    fun woof() {
        println("woof")
    }
}

fun perform(animal: Animal) {
    val kFunction: KFunction<*> = when (animal) {
        is Cat -> animal::meow
        is Dog -> animal::woof
    }
    kFunction.call()
}

fun main() {
    perform(Cat())
}

在动物变量智能转换为特定类型 Cat 和 Dog 之后,您可以使用不同的成员引用 animal::meow 和 animal::woof。在类型检查之后,您可以访问与子类型对应的成员引用。

更出色的可调用引用推理

现在,可以更方便地使用包含默认参数值的函数的可调用引用。例如,以下 foo 函数的可调用引用可以解释为获取一个 Int 参数或不获取参数:

fun foo(i: Int = 0): String = "$i!"

fun apply1(func: () -> String): String = func()
fun apply2(func: (Int) -> String): String = func(42)

fun main() {
    println(apply1(::foo))
    println(apply2(::foo))
}

更出色的委托属性推理

之前,在分析遵循 by 关键字的委托表达式时,不会考虑委托属性的类型。例如,之前不会编译以下代码,但现在编译器可以正确地将 old 和 new 参数的类型推断为 String?

import kotlin.properties.Delegates

fun main() {
    var prop: String? by Delegates.observable(null) { p, old, new ->
        println("$old → $new")
    }
    prop = "abc"
    prop = "xyz"
}

 

语言变更

大多数语言变更在之前的博文中已有介绍:

在这篇博文中,我们将重点介绍与协定相关的一些小幅改进。

协定支持

定义自定义协定的语法仍为实验性功能,但我们已支持几种新的用例,其中协定可能会很有用。您现在可以使用具体化的泛型类型参数来定义协定。

例如,您可以为 assertIsInstance 函数实现以下协定:

@OptIn(ExperimentalContracts::class)
inline fun <reified T> assertIsInstance(value: Any?) {
    contract {
        returns() implies (value is T)
    }

    assertTrue(value is T)
}

由于 T 类型参数已具体化,您可以在函数主体中检查它的类型。现在,这一点在协定中也可以实现。一个包含断言消息的相似函数稍后将添加到 kotlin.test 库中。

另外,您现在还可以为 final 成员定义自定义协定。之前,为成员函数定义协定是完全禁止的,因为在层次结构中为一些成员定义协定意味着也需要定义相应协定的层次结构,而且在设计和讨论方面也存在问题。不过,如果成员函数为 final,且不会重写任何其他函数,则可以安全地为它定义协定。

标准库变更

排除弃用的实验性协同程序

在 1.3.0 中,已弃用 kotlin.coroutines.experimental API,而支持 kotlin.coroutines。在 1.4-M1 中,我们将 kotlin.coroutines.experimental 从标准库中移除,彻底完成了它的弃用周期。对于仍在 JVM 上使用它的用户,我们提供兼容性工件 kotlin-coroutines-experimental-compat.jar 以及所有实验性协同程序 API。我们准备将它发布到 Maven 并包含在标准库以外的 Kotlin 分发中。当前,我们已经将它与 1.4-M1 工件一同发布到 bintray 存储库。

移除弃用的 mod 运算符

另一个弃用的函数是数值类型的 mod 运算符,这个运算符会在除法运算之后计算余数。在 Kotlin 1.1 中,此运算符被 rem() 函数替代。现在,我们将它从标准库中完全移除。

从浮动类型到 Byte 和 Short 转换的弃用

标准库包含将浮点数转换为整数类型的函数:toInt()toShort()toByte()。将浮点数转换为 Short 和 Byte 可能导致意外结果,因为值范围和变量大小较小。为了避免这种问题,从 1.4-M1 起,我们将弃用 Double 和 Float 类型的函数 toShort() 和 toByte()。如果您仍需要将浮点数转换为 Byte 或 Short,请进行两步转换:首先转换为 Int,然后转换为目标类型。

常用反射 API

我们修改了常用反射 API。现在,它仅包含可以在所有三个目标平台(JVM、JS、Native)上使用的成员,这样您就可以确保同一代码可以用于任何一个平台。

use() 和时间测量函数的新协定

我们将在标准库中扩大协定的使用。在 1.4-M1 中,我们添加了一些协定,可以为 use() 函数与时间测量函数 measureTimeMillis() 和 measureNanoTime() 声明代码块的单次执行。

Kotlin 反射的 Proguard 配置

从 1.4-M1 开始,我们为 kotlin-reflect.jar 中的 Kotlin 反射嵌入了 Proguard/R8 配置。这样,使用 R8 或 Proguard 的大多数 Android 项目无需额外的配置就可以使用 kotlin-reflect。您无需再为 kotlin-reflect 内部项复制粘贴 Proguard 规则。但是请注意,您仍需要明确地列出要在上面反射的所有 API。

Kotlin/JVM

从 1.3.70 版起,Kotlin 可以在 JVM 字节码(目标版本 1.8+)中生成类型注解,以便它们在运行时可用。社区请求此功能已有一段时间,因为它让使用某些现有 Java 库更加容易,并且为新库的作者提供了更多功能。

在以下示例中,可以将 String 类型上的 @Foo 注解发出到字节码,然后由库代码使用:

@Target(AnnotationTarget.TYPE)
annotation class Foo

class A {
    fun foo(): @Foo String = "OK"
}

有关如何在字节码中发出类型注解的详细信息,请参阅 Kotlin 1.3.70 版本博文的相关部分。

Kotlin/JS

对于 Kotlin/JS,此里程碑包含对 Gradle DSL 的变更,这是包含新的 IR 编译器后端的第一个版本,新编译器带来了优化和新功能。

Gradle DSL 变更

在 kotlin.js 和 multiplatform Gradle 插件中,引入了一个重要的新设置。在 build.gradle.kts 文件中的目标块内,现已支持 produceExecutable(),如果您想要在构建时生成 .js 工件,则必须使用它:

kotlin {
    target {
        useCommonJs()

        produceExecutable()
        
        browser {}
    }
}

如果您要编写 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 是包含可用于 Kotlin/JS 目标的新 IR 编译器后端的第一个版本。此后端是显著改进的优化的基础,也是 Kotlin/JS 与 JavaScript 和 TypeScript 交互方式变更的决定性因素。下面重点介绍的功能都针对新的 IR 编译器后端。尽管还没有默认启用,我们鼓励您在项目中试用它,开始为新的后端准备库,并向我们提供反馈,记录遇到的问题。

使用新后端

要开始使用新后端,请在您的 gradle.properties 文件中设置下列标志:

kotlin.js.compiler=ir

如果需要为 IR 编译器后端和默认后端生成库,您还可以将此标志设置为 both。此标志的确切功能在本博文的 Both 模式部分中进行了介绍。此标志非常有必要,因为新的和默认编译器后端不兼容二进制文件。

无二进制兼容性

新的 IR 编译器后端的主要变化是缺少与默认后端的二进制兼容性。在 Kotlin/JS 的两种后端之间缺少这种兼容性意味着使用新的 IR 编译器后端创建的库无法用于默认后端,反之亦然。

如果您想要将 IR 编译器后端用于项目,则需要将所有 Kotlin 依赖项更新为支持此新后端的版本。由 JetBrains 在 Kotlin 1.4-M1 中面向 Kotlin/JS 发布的库已包含与新的 IR 编译器后端搭配使用而需要的所有工件。依赖这种库时,Gradle 会自动选择正确的工件(即无需指定 IR 特定的坐标)。请注意,一些库(如 kotlin-wrappers)在使用新的 IR 编译器后端时会出问题,因为它们依赖于默认后端的特定特性。我们已经意识到这一点,以后将改进此功能。

如果您是库作者,期待着能够兼容当前的编译器后端和新的 IR 编译器后端,另请查看本博文的“Both 模式”部分。下一部分将详细介绍新编译器的好处和差异。

优化的 DCE

与默认后端相比,新 IR 编译器后端进行了显著优化。生成的代码能够更好地与静态分析器一同使用,甚至还可以通过 Google 的 Closure Compiler 从新 IR 编译器后端运行生成的代码,并使用它的高级模式优化(请注意,Kotlin/JS Gradle 插件对此不提供特定支持)。

最明显的变化是生成工件的代码大小。消除死代码的改进方法使工件可以大幅缩小。例如,这将使“Hello, World!”Kotlin/JS 程序减小到小于 1.7 KiB。对于更复杂的(演示)项目,例如使用 kotlinx.coroutines 的此示例项目,数值也会显著变化,希望事实可以说明这一切:

默认后端

IR 后端

编译后

3.9 MiB

1.1 MiB

JS DCE 后

713 KiB

430 KiB

捆绑后

329 KiB

184 KiB

ZIP 后

74 KiB

40 KiB

如果您还不相信,请自己试试。Kotlin 1.4-M1 已为两种后端默认启用 DCE 和捆绑!

将声明导出到 JavaScript 中

使用 IR 编译器后端时,标记为公开的声明将不再自动导出(即使名称毫无逻辑的版本也不会)。这是因为 IR 编译器的 closed-world 模型假设导出的声明会明确地注解,跟上一个一样,这也是一个有助于优化的因素。

要使 JavaScript 或 TypeScript 可以从外部使用顶级声明,请使用 @JsExport 注解。在以下示例中,我们使 KotlinGreeter(及其方法)和 farewell() 可以从 JavaScript 使用,但使 secretGreeting() 仅适用于 Kotlin:

package blogpost

@JsExport
class KotlinGreeter(private val who: String) {
    fun greet() = "Hello, $who!"
}

@JsExport
fun farewell(who: String) = "Bye, $who!"

fun secretGreeting(who: String) = "Sup, $who!" // only from Kotlin!

预览:TypeScript 定义

在新的 Kotlin/JS IR 编译器中,我们很高兴展示的另一个功能是从 Kotlin 代码生成 TypeScript 定义。在开发混合应用时,JavaScript 工具和 IDE 可以使用这些定义来提供自动补全、支持静态分析器,并更轻松地在 JS 和 TS 项目中包含 Kotlin 代码。

在配置为使用 produceExecutable() 的项目中,对于使用 @JsExport(参见上文)标记的顶级声明,将生成包含 TypeScript 定义的 .d.ts 文件。对于上面的代码段,它们是这样的:

// [...]
namespace blogpost {
    class KotlinGreeter {
        constructor(who: string)
        greet(): string
    }
    function farewell(who: string): string
}
// [...]

在 Kotlin 1.4-M1 中,可以在未使用 webpack 打包的相应 JavaScript 代码旁的 build/js/packages/<package_name>/kotlin 中找到这些声明。请注意,由于现在只是预览版本,它们默认没有添加到 distributions 文件夹中。您可以期待此行为将来会发生变化。

Both 模式

为了让库维护者更方便地迁移到新的 IR 编译器后端,为 gradle.properties 中的 kotlin.js.compiler 标志引入了一个额外设置:

kotlin.js.compiler=both

在 both 模式下,从您的源代码构建库时会使用 IR 编译器后端和默认编译器后端(因此得名)。这意味着会生成用于 Kotlin IR 的 klib 文件和用于默认编译器的 js 文件。在同一个 Maven 坐标下发布时,Gradle 会根据用例自动选择正确的工件:为旧编译器选择 js,为新编译器选择 klib。这表示您可以使用新的 IR 编译器后端编译和发布库,新的 IR 编译器后端适用于已升级到 Kotlin 1.4-M1 的项目和使用任意一种编译器后端的项目。这有助于确保仍在使用默认后端的用户不会受影响——假定他们已经将项目升级到 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 编译器选项。

binaries.framework {
     freeCompilerArgs += "-Xno-objc-generics"
}

请注意,文档中列出的所有详细信息和限制仍有效。

在 Objective-C/Swift 互操作中处理异常的变更

在 1.4 中,针对转换异常的方式,我们将稍微变更从 Kotlin 生成的 Swift API。Kotlin 和 Swift 之间的错误处理存在着根本的区别。所有 Kotlin 异常都未经检查,而 Swift 只包含检查的错误。因此,要使 Swift 代码感知预期的异常,Kotlin 函数应使用 @Throws 注解标记,此注解会指定一系列潜在的异常类。
编译为 Swift 或 Objective-C 框架时,拥有或要继承 @Throws 注解的函数在 Objective-C 中表示为产生方法的 NSError*,在 Swift 中表示为 throws 方法。
之前,除了 RuntimeException 和 Error 以外的任何异常都传播为 NSError。在 1.4-M1 中,我们更改了此行为。现在,仅对一些异常引发 NSError,这些异常是指定为 @Throws 注解的参数的类实例(或其子类)。影响 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 IDEA 和 Android Studio 中,您可以将 Kotlin 插件更新为 1.4-M1。查看如何执行此操作

如果您想处理在安装该预览版之前创建的现有项目,则需要在 Gradle 或 Maven 中针对预览版配置您的构建

您可以从 Github 发布页面下载命令行编译器

您可以使用随此版本一起发布的以下

您也可以在此处找到版本详细信息和兼容库的列表。

分享您的反馈

如果您发现错误并在 YouTrack 问题跟踪器中报告,我们将不胜感激。我们将尽力在最终版本之前修复所有重要问题,也就是说,您不用等到下一个 Kotlin 版本即可看到问题得到解决。

如果您有任何问题并想参与讨论,欢迎加入 Kotlin Slack 中的 #eap 频道(在此处获取邀请)。在此频道中,您还可以获取有关新预览版本的通知。

Let’s Kotlin!

外部贡献

特别感谢 Zac Sweers 将 Proguard 配置嵌入 kotlin-reflect 的贡献。

我们要感谢所有的外部贡献者,此版本中包含了他们的拉取请求:

原文发表于 2020 年 3 月 20 日,作者 Sebastian Aigner