13.6.11.1 Kotlin 1.3 的新变化

原文链接: https://kotlinlang.org/docs/whatsnew13.html

13.6.11.1 Kotlin 1.3 的新变化

阅读 Kotlin 1.3 的发行说明,涵盖新的语言特性、Kotlin Multiplatform、JVM、Native、JS 的更新,以及对 Gradle 和 Maven 构建工具的支持。

发布日期:2018 年 10 月 29 日

提示: 关于 Kotlin 发布周期的信息,请参阅 Kotlin 发布流程。

协程正式发布

经过长期而广泛的实战检验,协程现在正式发布了!这意味着从 Kotlin 1.3 起,语言支持和 API 都完全稳定。请查看新的协程概览页面。

Kotlin 1.3 引入了对挂起函数的可调用引用,并在反射 API 中支持协程。

Kotlin/Native

Kotlin 1.3 继续改进和打磨 Native 目标。详情请参阅 Kotlin/Native 概览。

多平台项目

在 1.3 中,我们彻底重做了多平台项目的模型,以提升表达能力和灵活性,并让共享公共代码更容易。此外,Kotlin/Native 现在也作为受支持的目标之一!

与旧模型的关键区别是:

  • 在旧模型中,公共代码和平台特有代码需要放在不同模块中,通过 expectedBy 依赖关联。现在,公共代码和平台特有代码放在同一模块的不同源根中,项目配置更容易。
  • 现在针对不同的受支持平台有大量预置平台配置。
  • 依赖配置发生了变化;现在为每个源根单独指定依赖。
  • 源集现在可以在任意平台子集之间共享(例如在面向 JS、Android 和 iOS 的模块中,可以有一个只在 Android 和 iOS 之间共享的源集)。
  • 现在支持发布多平台库。

更多信息请参阅多平台编程文档。

契约

Kotlin 编译器会做大量静态分析来提供警告并减少样板代码。其中最显著的特性之一是智能转换 —— 它能够根据已执行的类型检查自动进行转换:

1
2
3
fun foo(s: String?) {
    if (s != null) s.length // 编译器自动把 's' 转换为 'String'
}

然而,一旦把这些检查提取到单独的函数中,所有智能转换就立刻消失了:

1
2
3
4
5
fun String?.isNotNull(): Boolean = this != null

fun foo(s: String?) {
    if (s.isNotNull()) s.length // 没有智能转换 :(
}

为改善这类情况下的行为,Kotlin 1.3 引入了名为契约的实验性机制。

契约允许函数以编译器能够理解的方式显式描述自身的行为。目前支持两大类情况:

  • 通过声明函数调用结果与所传实参值之间的关系来改进智能转换分析:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun require(condition: Boolean) {
    // 这是一种语法形式,它告诉编译器:
    // “如果这个函数成功返回,那么所传的 'condition' 为 true”
    contract { returns() implies condition }
    if (!condition) throw IllegalArgumentException(...)
}

fun foo(s: String?) {
    require(s is String)
    // 这里 s 被智能转换为 'String',否则
    // 'require' 就会抛出异常
}
  • 在存在高阶函数的情况下改进变量初始化分析:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fun synchronize(lock: Any?, block: () -> Unit) {
    // 它告诉编译器:
    // “这个函数会在此刻、并且恰好调用一次 'block'”
    contract { callsInPlace(block, EXACTLY_ONCE) }
}

fun foo() {
    val x: Int
    synchronize(lock) {
        x = 42 // 编译器知道传给 'synchronize' 的 lambda 会被调用
               // 恰好一次,因此不会报告重复赋值
    }
    println(x) // 编译器知道该 lambda 一定会被调用并执行
               // 初始化,因此这里认为 'x' 已初始化
}

标准库中的契约

stdlib 已经在使用契约,从而带来了上述分析的改进。契约的这一部分是稳定的,也就是说你现在无需任何额外的选择启用就能受益于改进后的分析:

1
2
3
4
5
6
7
8
9
fun bar(x: String?) {
    if (!x.isNullOrEmpty()) {
        println("length of '$x' is ${x.length}") // 太好了,智能转换为非空!
    }
}
fun main() {
    bar(null)
    bar("42")
}

自定义契约

你也可以为自己的函数声明契约,但该特性是实验性的,因为当前语法仍处于早期原型阶段,很可能会发生变化。另请注意,目前 Kotlin 编译器不会校验契约,因此编写正确且可靠的契约是程序员的责任。

自定义契约通过调用 contract 标准库函数引入,它提供 DSL 作用域:

1
2
3
4
5
6
fun String?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }
    return this == null || isEmpty()
}

关于语法细节以及兼容性说明,请参阅 KEEP。

把 when 的主语捕获到变量中

在 Kotlin 1.3 中,现在可以把 when 的主语捕获到变量中:

1
2
3
4
5
fun Request.getBody() =
        when (val response = executeRequest()) {
            is Success -> response.body
            is HttpError -> throw HttpException(response.status)
        }

虽然此前可以在 when 之前提取这个变量,但 when 中的 val 会把它正确的作用域限制在 when 的主体中,从而避免污染命名空间。在此查看 when 的完整文档。

接口伴生对象中的 @JvmStatic 和 @JvmField

在 Kotlin 1.3 中,可以用 @JvmStatic 和 @JvmField 注解标记接口的 companion 对象的成员。在 class 文件中,这些成员会被提升到相应的接口中并标记为 static。

例如,以下 Kotlin 代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface Foo {
    companion object {
        @JvmField
        val answer: Int = 42

        @JvmStatic
        fun sayHello() {
            println("Hello, world!")
        }
    }
}

等价于这样的 Java 代码:

1
2
3
4
5
6
interface Foo {
    public static int answer = 42;
    public static void sayHello() {
        // ...
    }
}

注解类中的嵌套声明

在 Kotlin 1.3 中,注解可以有嵌套的类、接口、对象和伴生对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
annotation class Foo {
    enum class Direction { UP, DOWN, LEFT, RIGHT }

    annotation class Bar

    companion object {
        fun foo(): Int = 42
        val bar: Int = 42
    }
}

无参数的 main

按照约定,Kotlin 程序的入口点是签名为 main(args: Array<String>) 的函数,其中 args 表示传给程序的命令行参数。然而并非每个应用都支持命令行参数,因此这个参数往往最终没有被使用。

Kotlin 1.3 引入了更简单的、不带参数的 main 形式。现在 Kotlin 中的 “Hello, World” 短了 19 个字符!

1
2
3
fun main() {
    println("Hello, world!")
}

高元数的函数

在 Kotlin 中,函数类型表示为接受不同数量参数的泛型类:Function0<R>、Function1<P0, R>、Function2<P0, P1, R>…… 这种方式的问题在于这个列表是有限的,目前以 Function22 结束。

Kotlin 1.3 放宽了这一限制,并添加对更高元数函数的支持:

1
2
3
fun trueEnterpriseComesToKotlin(block: (Any, Any, ... /* 还有 42 个 */, Any) -> Any) {
    block(Any(), Any(), ..., Any())
}

渐进模式

Kotlin 非常在意代码的稳定性和向后兼容性:Kotlin 兼容性政策规定,破坏性变更(例如让原本能正常编译的代码不再能编译的变更)只能在大版本(1.2、1.3 等)中引入。

我们相信许多用户希望有更快的循环,让关键的编译器缺陷修复立即到位,使代码更安全、更正确。因此 Kotlin 1.3 引入了渐进编译器模式,可以通过向编译器传入参数 -progressive 来启用。

在渐进模式下,语言语义中的某些修复可以立即生效。所有这些修复都有两个重要特性:

  • 它们保持源代码与旧编译器的向后兼容性,也就是说,所有能被渐进式编译器编译的代码也能被非渐进式编译器正常编译。
  • 它们只在某种意义上让代码更安全 —— 例如可以禁止某些不健全的智能转换、把生成代码的行为改为更可预测/更稳定等等。

启用渐进模式可能需要你重写一部分代码,但不会太多 —— 渐进模式下启用的所有修复都经过精心挑选、审查,并配有工具化迁移辅助。我们预计渐进模式会成为任何积极维护、且会快速更新到最新语言版本的代码库的不错选择。

内联类

内联类处于 Alpha 阶段。它们可能发生不兼容的变化,并在将来需要手动迁移。我们非常欢迎你在 YouTrack 上提供反馈。详见参考文档。

Kotlin 1.3 引入了一种新的声明 —— inline class。内联类可以看作普通类的受限版本,特别之处在于内联类必须恰好有一个属性:

1
inline class Name(val s: String)

Kotlin 编译器会利用这一限制积极地优化内联类的运行时表示,并在可能的情况下用底层属性的值替换其实例,从而消除构造器调用、GC 压力,并启用其他优化:

1
2
3
4
5
6
7
inline class Name(val s: String)
fun main() {
    // 下一行不会发生构造器调用,而且
    // 运行时 'name' 只包含字符串 "Kotlin"
    val name = Name("Kotlin")
    println(name.s)
}

关于内联类的细节,请参阅参考文档。

无符号整数

警告: 无符号整数处于 Beta 阶段。它们的实现已接近稳定,但将来可能仍需要迁移步骤。我们会尽力把你需要做的改动降到最少。

Kotlin 1.3 引入了无符号整数类型:

  • kotlin.UByte:无符号 8 位整数,范围是 0 到 255
  • kotlin.UShort:无符号 16 位整数,范围是 0 到 65535
  • kotlin.UInt:无符号 32 位整数,范围是 0 到 2^32 - 1
  • kotlin.ULong:无符号 64 位整数,范围是 0 到 2^64 - 1

有符号类型的大部分功能也支持无符号版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fun main() {
// 你可以使用字面量后缀定义无符号类型
val uint = 42u
val ulong = 42uL
val ubyte: UByte = 255u

// 你可以通过标准库扩展在有符号类型与无符号类型之间转换:
val int = uint.toInt()
val byte = ubyte.toByte()
val ulong2 = byte.toULong()

// 无符号类型支持类似的运算符:
val x = 20u + 22u
val y = 1u shl 8
val z = "128".toUByte()
val range = 1u..5u
println("ubyte: $ubyte, byte: $byte, ulong2: $ulong2")
println("x: $x, y: $y, z: $z, range: $range")
}

细节请参阅参考文档。

@JvmDefault

@JvmDefault 是实验性的。它可能随时被移除或更改。请仅将其用于评估目的。我们非常欢迎你在 YouTrack 上提供反馈。

Kotlin 面向众多 Java 版本,包括不允许接口中存在默认方法的 Java 6 和 Java 7。为方便你使用,Kotlin 编译器绕过了这一限制,但该变通方案与 Java 8 引入的 default 方法不兼容。

这可能给 Java 互操作带来问题,因此 Kotlin 1.3 引入了 @JvmDefault 注解。带该注解的方法会为 JVM 生成为 default 方法:

1
2
3
4
5
interface Foo {
    // 会生成为 'default' 方法
    @JvmDefault
    fun foo(): Int = 42
}

警告!使用 @JvmDefault 注解你的 API 会对二进制兼容性产生严重影响。 在生产环境中使用 @JvmDefault 之前,请务必仔细阅读参考页面。

标准库

多平台随机数

在 Kotlin 1.3 之前,没有在所有平台上生成随机数的统一方式 —— 我们不得不求助于平台特有的方案,例如 JVM 上的 java.util.Random。本次发布通过引入在所有平台上都可用的 kotlin.random.Random 类解决了该问题:

1
2
3
4
5
6
import kotlin.random.Random

fun main() {
    val number = Random.nextInt(42) // number 位于 [0, limit) 范围内
    println(number)
}

isNullOrEmpty 与 orEmpty 扩展

一些类型的 isNullOrEmpty 和 orEmpty 扩展已经存在于标准库中。前者在接收者为 null 或为空时返回 true,后者在接收者为 null 时回退到空实例。Kotlin 1.3 为集合、映射和对象数组提供了类似的扩展。

在两个已有数组之间复制元素

针对现有数组类型(包括无符号数组)的 array.copyInto(targetArray, targetOffset, startIndex, endIndex) 函数,让用纯 Kotlin 实现基于数组的容器更容易。

1
2
3
4
5
6
7
8
fun main() {
    val sourceArr = arrayOf("k", "o", "t", "l", "i", "n")
    val targetArr = sourceArr.copyInto(arrayOfNulls<String>(6), 3, startIndex = 3, endIndex = 6)
    println(targetArr.contentToString())

    sourceArr.copyInto(targetArr, startIndex = 0, endIndex = 3)
    println(targetArr.contentToString())
}

associateWith

有一个键的列表、并希望把每个键与某个值关联起来构建映射,是相当常见的情况。以前可以用 associate { it to getValue(it) } 函数实现,但现在我们引入了更高效、更易探索的替代方案:keys.associateWith { getValue(it) }。

1
2
3
4
5
fun main() {
    val keys = 'a'..'f'
    val map = keys.associateWith { it.toString().repeat(5).capitalize() }
    map.forEach { println(it) }
}

ifEmpty 与 ifBlank 函数

集合、映射、对象数组、字符序列和序列现在都有 ifEmpty 函数,它允许指定一个回退值,在接收者为空时用它代替接收者:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main() {
    fun printAllUppercase(data: List<String>) {
        val result = data
        .filter { it.all { c -> c.isUpperCase() } }
            .ifEmpty { listOf("<no uppercase>") }
        result.forEach { println(it) }
    }

    printAllUppercase(listOf("foo", "Bar"))
    printAllUppercase(listOf("FOO", "BAR"))
}

字符序列和字符串还有一个 ifBlank 扩展,它的作用与 ifEmpty 相同,但检查的是字符串是否全部为空白字符而不是是否为空。

1
2
3
4
5
fun main() {
    val s = "    \n"
    println(s.ifBlank { "<blank>" })
    println(s.ifBlank { null })
}

反射中的密封类

我们为 kotlin-reflect 添加了新的 API,可用于枚举 sealed 类的所有直接子类型,即 KClass.sealedSubclasses。

更小的变化

  • Boolean 类型现在有了伴生对象。
  • 新增 Any?.hashCode() 扩展,对 null 返回 0。
  • Char 现在提供 MIN_VALUE 和 MAX_VALUE 常量。
  • 原始类型伴生对象中新增 SIZE_BYTES 和 SIZE_BITS 常量。

工具

IDE 中的代码风格支持

Kotlin 1.3 在 IntelliJ IDEA 中引入了对推荐的代码风格的支持。迁移指南请查看这个页面。

kotlinx.serialization

kotlinx.serialization 是一个为 Kotlin 提供多平台(反)序列化对象支持的库。此前它是一个单独的项目,但从 Kotlin 1.3 起,它与其他编译器插件一样随 Kotlin 编译器发行版一起提供。主要区别在于你无需再手动关注 Serialization IDE 插件与你所用 Kotlin IDE 插件版本的兼容性:现在 Kotlin IDE 插件已经内置了序列化支持!

详情请见这里。

警告: 尽管 kotlinx.serialization 现在随 Kotlin 编译器发行版一起提供,但它在 Kotlin 1.3 中仍被视为实验性特性。

脚本功能更新

脚本功能是实验性的。它可能随时被移除或更改。请仅将其用于评估目的。我们非常欢迎你在 YouTrack 上提供反馈。

Kotlin 1.3 继续演进和改进脚本 API,引入了一些对脚本自定义的实验性支持,例如添加外部属性、提供静态或动态依赖等等。

更多细节请参阅 KEEP-75。

Scratch 文件支持

Kotlin 1.3 引入了对可运行的 Kotlin scratch 文件的支持。Scratch 文件是扩展名为 .kts 的 Kotlin 脚本文件,你可以直接编辑器中运行并获得求值结果。

详情请参阅通用的 Scratch 文档。