Edit Page

Kotlin 1.3 版中的新功能

最终更新: 2024/03/21

发布日期: 2018/10/29

协程功能正式发布

经过长期广泛的实战测试之后, 协程功能终于正式发布了! 也就是说, 从 Kotlin 1.3 开始, 协程功能的语言级支持, 以及 API 都进入 完全稳定 状态. 请参见新的 协程概述 文档.

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

Kotlin/Native

Kotlin 1.3 继续改进对原生程序开发的. 详情请参见 Kotlin/Native 概述.

跨平台项目

在 1.3 中, 我们完成了对跨平台项目模式的重构工作, 改进了表达能力和灵活性, 使得共用代码变得更加容易. 而且, Kotlin/Native 现在也是我们支持的目标平台之一了!

与旧模式的主要不同在于:

  • 在旧模式中, 共通代码和平台相关代码需要放在不同的模块中, 然后使用 expectedBy 依赖项导入. 现在, 共通代码和平台相关代码放在同一模块的不同源代码路径中, 项目配置变得更加容易.
  • 对于支持的各种目标平台, 现在有了大量的 预定义平台配置.
  • 依赖项配置有了变化; 现在以各个源代码路径为单位分别指定依赖项.
  • 源代码集现在可以在任意一部分平台之间共用(比如, 在编译目标平台为 JS, Android 和 iOS 的模块中, 你可以让某个源代码集只在 Android 和 iOS 平台中共用).
  • 现在支持 发布跨平台的库.

更多详细信息, 请参见 跨平台程序开发文档.

契约(Contract)

Kotlin 编译器会进行大量的静态分析, 产生警告信息, 并减少样板代码. 其中最值得注意的功能之一就是智能类型转换 — 根据已有的类型检查代码, 可以自动进行类型转换:

fun foo(s: String?) {
    if (s != null) s.length // 编译器自动将 's' 转换为 'String' 类型
}

但是, 一旦将这些类型检查抽取到一个独立的函数中, 这些智能类型转换就消失了:

fun String?.isNotNull(): Boolean = this != null

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

为了改进这种情况下的编译器能力, Kotlin 1.3 引入了一个实验性的机制, 名为 契约 (contract).

契约 允许一个函数以编译器能够理解的方式明确地描述它的行为. 目前, 支持两打大类使用场景:

  • 声明一个函数调用的入口参数与输出结果之间的关系, 来改进编译器的智能类型转换分析能力:
fun require(condition: Boolean) {
    // 这是一个语法形式, 告诉编译器:
    // "如果这个函数成功地返回, 那么传入到这个函数内的 'condition' 为 true"
    contract { returns() implies condition }
    if (!condition) throw IllegalArgumentException(...)
}

fun foo(s: String?) {
    require(s is String)
    // 这里 's' 会被智能转换为 'String', 因为, 如果它不是 'String',
    // 'require' 应该抛出异常
}
  • 出现高阶函数时, 改进编译器的变量初始化分析能力:
fun synchronize(lock: Any?, block: () -> Unit) {
    // 这段代码告诉编译器:
    // "这个函数会立即调用 'block', 而且只调用一次"
    contract { callsInPlace(block, EXACTLY_ONCE) }
}

fun foo() {
    val x: Int
    synchronize(lock) {
        x = 42 // 编译器知道传递给 'synchronize' 的 lambda 表达式会被刚好调用一次
               // 因此不会报告 'x' 被多次赋值的错误
    }
    println(x) // 编译器知道 lambda 表达式一定会被调用一次, 并执行对 'x' 的初始化
               // 因此在这里会认为 'x' 已被初始化
}

标准库中的契约

stdlib 已经使用了契约, 用来改进上文介绍的编译器分析能力. 这部分契约是 稳定 的, 也就是说你不必添加额外的编译选项, 也能得到编译器分析能力的提高:

//sampleStart
fun bar(x: String?) {
    if (!x.isNullOrEmpty()) {
        println("length of '$x' is ${x.length}") // 哇~~~, 可以智能转换为非空类型!
    }
}
//sampleEnd
fun main() {
    bar(null)
    bar("42")
}

自定义的契约

也可以为你自己的函数声明契约, 但这个功能还是 实验性 的, 因为契约目前的语法还处于早期原型阶段, 将来很可能会改变. 而且请注意, 目前 Kotlin 编译器不会去验证契约的内容, 因此程序员需要自己负责编写正确而且完整的契约.

通过调用标注库的 contract 函数, 就可以声明自定义的契约, 这个函数会产生一个 DSL 作用域:

fun String?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }
    return this == null || isEmpty()
}

关于契约的语法, 以及兼容性问题, 详情请参见 KEEP.

when 语句的判定对象保存到变量中

在 Kotlin 1.3 中, 可以将 when 语句的判定对象保存到变量中:

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 中, 可以对接口的 companion 对象的成员标记 @JvmStatic@JvmField 注解. 在编译产生的类文件中, 这些成员会被提升到对应的接口内, 并变为 static 成员.

比如, 以下 Kotlin 代码:

interface Foo {
    companion object {
        @JvmField
        val answer: Int = 42

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

等价于以下 Java 代码:

interface Foo {
    public static int answer = 42;
    public static void sayHello() {
        // ...
    }
}

注解类中的嵌套声明

在 Kotlin 1.3 中, 注解可以拥有嵌套的类, 接口, 对象, 以及同伴对象:

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 函数形式, 它可以没有任何参数. "Hello, World" 程序在 Kotlin 代码中可以减少 19 个字符了!

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

带巨量参数的函数

在 Kotlin 中, 函数类型被表达为一个泛型, 接受不同数量的参数: Function0<R>, Function1<P0, R>, Function2<P0, P1, R>, … 这种方案存在的一个问题就是, 参数个数是有限的, 目前只支持到 Function22.

Kotlin 1.3 放宽了这个限制, 支持更多参数的函数:

fun trueEnterpriseComesToKotlin(block: (Any, Any, ... /* 另外还有 42 个 */, Any) -> Any) {
    block(Any(), Any(), ..., Any())
}

渐进模式

Kotlin 非常关注稳定性, 以及源代码的向后兼容: Kotlin 的兼容性政策是: 破坏性变更 (也就是, 某些变更会造成过去能够成功编译的代码无法编译) 只能出现在主版本中 (1.2, 1.3, 等等.).

我们相信, 很多用户会使用更快速的升级, 对于严重的编译器 bug 可以立即得到修正, 使得代码更加安全, 更加正确. 因此, Kotlin 1.3 引入了 渐进式 编译模式, 可以向编译器添加 -progressive 参数来启用这个模式.

在渐进模式下, 会立即启用某些语法层面的修正. 这些修正包含两个重要的特性:

  • 这些修正保证源代码在旧版本编译器上的向后兼容性, 也就是说, 凡是渐进模式下能够编译的代码, 在非渐进模式下也能正确编译.
  • 这些修正只会让代码 更正确 — 比如, 有些不适当的智能类型转换会被禁止, 编译产生的代码的行为可能会被修改, 变得更可预测, 更加稳定, 等等.

启用渐进模式可能会要求你重写某些代码, 但不会太多 — 渐进模式下启用的修正都经过仔细挑选, 检查, 并且提供了代码迁移的辅助工具. 对于那些活跃开发中, 快速更新语言版本的代码库, 我们期望渐进模式能够成为一个好的选择.

内联类

内联类目前处于 Alpha 阶段. 希望你能通过我们的 问题追踪系统 提供你的反馈意见. 详情请参见 参考文档.

Kotlin 1.3 引入了一种新的类型声明 — inline class. 内联类可以看作一种功能受到限制的类, 具体来说, 内联类只能有一个属性, 不能更多, 也不能更少:

inline class Name(val s: String)

Kotlin 编译器会使用这个限制, 尽力优化内联类的运行期表达, 用内联类底层属性的值来代替内联类的实例, 因此可以去除构造器调用, 减少 GC 压力, 而且可以进行进一步的代码优化:

inline class Name(val s: String)
//sampleStart
fun main() {
    // 下一行代码不会发生构造器调用, 而且在运行期 'name' 中只会包含字符串 "Kotlin"
    val name = Name("Kotlin")
    println(name.s)
}
//sampleEnd

关于内联类的详情, 请参见 参考文档.

无符号整数

无符号整数目前处于 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

有符号整数所支持的大多数功能, 对无符号整数也适用:

fun main() {
//sampleStart
// 可以使用字面值后缀来定义无符号整数
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
//sampleEnd
println("ubyte: $ubyte, byte: $byte, ulong2: $ulong2")
println("x: $x, y: $y, z: $z, range: $range")
}

详情请参见 参考文档.

@JvmDefault 注解

@JvmDefault 目前还处于实验性阶段. 请注意, 只为评估和试验目的来使用这个功能. 希望你能通过我们的 问题追踪系统 提供你的反馈意见.

Kotlin 支持许多 Java 版本, 包括 Java 6 和 Java 7, 在这些版本上还不支持接口的默认方法. 为了你编程的方便, Kotlin 编译器绕过了这个限制, 但是这个解决方法无法与 Java 8 中的 default 方法兼容.

这可能会造成与 Java 互操作时的问题, 因此 Kotlin 1.3 引入了 @JvmDefault 注解. 使用了这个注解的方法, 在 JVM 平台上会被编译为 default 方法:

interface Foo {
    // 会被编译为 'default' 方法
    @JvmDefault
    fun foo(): Int = 42
}

警告! 使用 @JvmDefault 注解来标注你的 API 会对二进制兼容性造成严重的影响. 在你的产品代码中使用 @JvmDefault 之前, 请一定要认真阅读 参考文档.

标准库

跨平台的 Random

在 Kotlin 1.3 之前, 没有统一的方法在所有的平台上生成随机数 — 我们必须使用各种平台独自的解决方案, 比如在 JVM 上使用 java.util.Random. Kotlin 1.3 版引入 kotlin.random.Random 类, 解决了这个问题, 这个类可以在所有的平台上使用:

import kotlin.random.Random

fun main() {
//sampleStart
    val number = Random.nextInt(42)  // 得到的随机数范围是 [0, limit)
    println(number)
//sampleEnd
}

isNullOrEmpty/orEmpty 扩展函数

标准库提供了对某些数据类型的 isNullOrEmptyorEmpty 扩展函数. 如果接受者是 null, 或内容为空, 那么 isNullOrEmpty 函数返回 true, 如果接受者是 null, 那么 orEmpty 函数返回一个不为 null, 但内容为空的实例. Kotlin 1.3 对集合(Collection), Map, 以及对象数组, 都提供了类似的扩展函数.

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

对既有的数组类型, 包括无符号整数数组, 提供了 array.copyInto(targetArray, targetOffset, startIndex, endIndex) 扩展函数, 可以使用纯 Kotlin 代码, 更简单地实现基于数组的容器.

fun main() {
//sampleStart
    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())
//sampleEnd
}

associateWith 函数

已有一组 key 值, 希望将每一个 Key 与某个值关联起来, 创建一个 Map, 这是很常见的情况. 以前, 使用 associate { it to getValue(it) } 函数, 也是可以做到的, 但是现在我们引入了一个更加高效, 而且更加易用的新函数: keys.associateWith { getValue(it) }.

fun main() {
//sampleStart
    val keys = 'a'..'f'
    val map = keys.associateWith { it.toString().repeat(5).capitalize() }
    map.forEach { println(it) }
//sampleEnd
}

ifEmpty 和 ifBlank 函数

对于集合(Collection), Map, 对象数组, 字符序列, 以及值序列(equence), 现在有了 ifEmpty 函数, 对于接受者对象内容为空的情况, 可以指定一个替代值:

fun main() {
//sampleStart
    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"))
//sampleEnd
}

除此之外, 字符序列和字符串还有一个 ifBlank 扩展函数, 它和 ifEmpty 函数一样, 也会使用指定的替代值, 但它检查的条件是字符串内容是否全部是空白字符.

fun main() {
//sampleStart
    val s = "    \n"
    println(s.ifBlank { "<blank>" })
    println(s.ifBlank { null })
//sampleEnd
}

在反射中使用封闭类

我们对 kotlin-reflect 添加了一个新的 API, 名为 KClass.sealedSubclasses, 可以用来得到 sealed 类的所有直接子类型.

细微变更

  • Boolean 类型现在带有同伴对象.
  • Any?.hashCode() 扩展函数, 对 null 值返回 0.
  • Char 现在带有 MIN_VALUEMAX_VALUE 常数.
  • 基本类型的同伴对象中增加了 SIZE_BYTESSIZE_BITS 常数.

工具

在 IDE 中支持代码风格

Kotlin 1.3 开始在 IntelliJ IDEA 中支持 推荐的代码风格. 关于代码迁移的方法, 请参见 参考文档.

kotlinx.serialization

kotlinx.serialization 是一个库, 在 Kotlin 中跨平台支持对象的序列化和反序列化. 以前它曾是一个独立的项目, 但从 Kotlin 1.3 起, 它和其他编译器 plugin 一样, 随 Kotlin 编译器一起发布. 主要的区别是, 你不需要手工维护 IDE 的序列化 Plugin 与你使用的 Kotlin IDE Plugin 之间的版本兼容问题: 因为现在 Kotlin IDE Plugin 已经包含了序列化功能!

详情请参见 参考文档.

虽然现在 kotlinx.serialization 与 Kotlin 编译器一起发布, 但在 Kotlin 1.3 中它仍然是一个实验性功能.

脚本 API 升级

脚本是一个实验性功能, 这个功能随时可能会放弃或发生修改. 请注意, 只为评估和试验目的来使用这个功能. 希望你能通过我们的 问题追踪系统 提供你的反馈意见.

Kotlin 1.3 仍在持续改进脚本 API, 引入了一些实验性的功能, 支持脚本的定制, 包括添加外部属性, 提供静态或动态的依赖项, 等等.

详情请参见, KEEP-75.

支持草稿文件(Scratch File)

Kotlin 1.3 开始支持可运行的 Kotlin 草稿文件(Scratch File). 草稿文件 是一个扩展名为 .kts 的 Kotlin 脚本文件, 你可以直接在编辑器中运行这个文件, 并得到执行结果.

详情请参见 草稿文件参考文档.