13.6.12 Kotlin 1.2 的新变化

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

13.6.12 Kotlin 1.2 的新变化

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

发布日期:2017 年 11 月 28 日

目录

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

多平台项目(实验性)

多平台项目是 Kotlin 1.2 中新的实验性特性,让你可以在 Kotlin 支持的目标平台之间复用代码 —— JVM、JavaScript,以及(未来的)Native。在多平台项目中,有三种模块:

  • 公共模块包含与任何平台都无关的代码,以及平台相关 API 的无实现声明。
  • 平台模块包含公共模块中平台相关声明在特定平台上的实现,以及其他平台相关代码。
  • 普通模块面向特定平台,既可以是平台模块的依赖,也可以依赖平台模块。

当你为特定平台编译多平台项目时,会同时生成公共部分和平台特有部分的代码。

多平台项目支持的一个关键特性是:可以通过 expected 和 actual 声明来表达公共代码对平台特有部分的依赖。expected 声明指定一个 API(类、接口、注解、顶层声明等)。actual 声明要么是该 API 的平台相关实现,要么是指向外部库中该 API 现有实现的类型别名。下面是一个例子:

在公共代码中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// expected 平台相关 API:
expect fun hello(world: String): String

fun greet() {
    // 使用 expected API:
    val greeting = hello("multiplatform world")
    println(greeting)
}

expect class URL(spec: String) {
    open fun getHost(): String
    open fun getPath(): String
}

在 JVM 平台代码中:

1
2
3
4
5
actual fun hello(world: String): String =
    "Hello, $world, on the JVM platform!"

// 使用现有的平台相关实现:
actual typealias URL = java.net.URL

关于构建多平台项目的细节和步骤,请参阅多平台编程文档。

其他语言特性

注解中的数组字面量

从 Kotlin 1.2 开始,注解的数组实参可以使用新的数组字面量语法传入,而不必使用 arrayOf 函数:

1
2
3
4
@CacheConfig(cacheNames = ["books", "default"])
public class BookRepositoryImpl {
    // ...
}

数组字面量语法仅限于注解实参。

lateinit 顶层属性和局部变量

lateinit 修饰符现在可以用于顶层属性和局部变量。后者可用于这样的情况:作为构造器实参传给某个对象的 lambda 引用了另一个需要稍后才定义的对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Node<T>(val value: T, val next: () -> Node<T>)

fun main(args: Array<String>) {
    // 三个节点构成的循环:
    lateinit var third: Node<Int>

    val second = Node(2, next = { third })
    val first = Node(1, next = { second })

    third = Node(3, next = { first })

    val nodes = generateSequence(first) { it.next() }
    println("Values in the cycle: ${nodes.take(7).joinToString { it.value.toString() }}, ...")
}

检查 lateinit 变量是否已初始化

现在你可以使用属性引用上的 isInitialized 检查 lateinit 变量是否已初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Foo {
    lateinit var lateinitVar: String

    fun initializationLogic() {
        println("isInitialized before assignment: " + this::lateinitVar.isInitialized)
        lateinitVar = "value"
        println("isInitialized after assignment: " + this::lateinitVar.isInitialized)
    }
}

fun main(args: Array<String>) {
	Foo().initializationLogic()
}

带默认函数式参数的内联函数

内联函数现在允许为其内联的函数式参数提供默认值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
inline fun <E> Iterable<E>.strings(transform: (E) -> String = { it.toString() }) =
    map { transform(it) }

val defaultStrings = listOf(1, 2, 3).strings()
val customStrings = listOf(1, 2, 3).strings { "($it)" }

fun main(args: Array<String>) {
    println("defaultStrings = $defaultStrings")
    println("customStrings = $customStrings")
}

显式转换中的信息用于类型推断

Kotlin 编译器现在可以利用类型转换中的信息进行类型推断。如果你调用一个返回类型参数 T 的泛型方法,并把返回值转换为特定类型 Foo,编译器现在会理解此次调用中的 T 需要绑定为类型 Foo。

这对 Android 开发者尤其重要,因为编译器现在可以正确分析 Android API 级别 26 中的泛型 findViewById 调用:

1
val button = findViewById(R.id.button) as Button

智能转换的改进

当变量由安全调用表达式赋值并被检查是否为 null 时,智能转换现在也会应用到安全调用的接收者上:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
fun countFirst(s: Any): Int {
    val firstChar = (s as? CharSequence)?.firstOrNull()
    if (firstChar != null)
    return s.count { it == firstChar } // s: Any 被智能转换为 CharSequence

    val firstItem = (s as? Iterable<*>)?.firstOrNull()
    if (firstItem != null)
    return s.count { it == firstItem } // s: Any 被智能转换为 Iterable<*>
    return -1
}

fun main(args: Array<String>) {
  val string = "abacaba"
  val countInString = countFirst(string)
  println("called on \"$string\": $countInString")

  val list = listOf(1, 2, 3, 1, 2)
  val countInList = countFirst(list)
  println("called on $list: $countInList")
}

此外,lambda 中的智能转换现在允许作用于只在 lambda 之前被修改的局部变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main(args: Array<String>) {
    val flag = args.size == 0
    var x: String? = null
    if (flag) x = "Yahoo!"

    run {
        if (x != null) {
            println(x.length) // x 被智能转换为 String
        }
    }
}

支持把 ::foo 作为 this::foo 的简写

对 this 的成员的有界可调用引用现在可以省略显式接收者,写成 ::foo 而不是 this::foo。这也让可调用引用在引用外部接收者成员的 lambda 中更方便使用。

破坏性变更:try 块之后可靠的智能转换

此前,Kotlin 会把 try 块内部的赋值用于该块之后的智能转换,这可能破坏类型安全和空安全并导致运行时失败。本次发布修复了该问题,使智能转换更严格,但会破坏一些依赖此类智能转换的代码。

要切换到旧的智能转换行为,请把回退标志 -Xlegacy-smart-cast-after-try 作为编译器参数传入。它将在 Kotlin 1.3 中被弃用。

弃用:data 类覆盖 copy

当 data 类派生自一个已经有相同签名 copy 函数的类型时,为 data 类生成的 copy 实现会使用父类的默认值,导致违反直觉的行为;如果父类没有默认参数,则会在运行时失败。

导致 copy 冲突的继承在 Kotlin 1.2 中已被弃用并给出警告,在 Kotlin 1.3 中将成为错误。

弃用:枚举条目中的嵌套类型

由于初始化逻辑中的问题,在枚举条目内部定义非 inner class 的嵌套类型已被弃用。这在 Kotlin 1.2 中会产生警告,并将在 Kotlin 1.3 中成为错误。

弃用:vararg 使用单个命名实参

为了与注解中的数组字面量保持一致,以命名形式为 vararg 参数传入单个元素(foo(items = i))已被弃用。请配合相应的数组工厂函数使用展开运算符:

1
foo(items = *arrayOf(1))

在这种情况下去除多余数组创建有一项优化,可以避免性能下降。单实参形式在 Kotlin 1.2 中会产生警告,并将在 Kotlin 1.3 中移除。

弃用:继承 Throwable 的泛型类的内部类

继承自 Throwable 的泛型类型的内部类可能在抛出-捕获场景中违反类型安全,因此已被弃用:在 Kotlin 1.2 中给出警告,在 Kotlin 1.3 中成为错误。

弃用:修改只读属性的幕后字段

在自定义 getter 中通过 field = ... 修改只读属性的幕后字段已被弃用:在 Kotlin 1.2 中给出警告,在 Kotlin 1.3 中成为错误。

标准库

Kotlin 标准库产物与拆分包

Kotlin 标准库现在与 Java 9 模块系统完全兼容,而后者禁止拆分包(多个 jar 文件声明同一个包中的类)。为了支持这一点,引入了新的产物 kotlin-stdlib-jdk7 和 kotlin-stdlib-jdk8,它们取代了旧的 kotlin-stdlib-jre7 和 kotlin-stdlib-jre8。

从 Kotlin 的角度看,新产物中的声明在相同的包名下可见,但对 Java 而言包名不同。因此,切换到新产物不需要对源代码做任何修改。

为确保与新模块系统兼容而做的另一项修改是:从 kotlin-reflect 库中移除了 kotlin.reflect 包中已弃用的声明。如果你此前在使用它们,需要改用 kotlin.reflect.full 包中的声明,该包自 Kotlin 1.1 起受支持。

windowed、chunked、zipWithNext

针对 Iterable<T>、Sequence<T> 和 CharSequence 的新扩展覆盖了诸如缓冲或批处理(chunked)、滑动窗口与计算滑动平均(windowed)以及处理相邻元素对(zipWithNext)等用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fun main(args: Array<String>) {
    val items = (1..9).map { it * it }

    val chunkedIntoLists = items.chunked(4)
    val points3d = items.chunked(3) { (x, y, z) -> Triple(x, y, z) }
    val windowed = items.windowed(4)
    val slidingAverage = items.windowed(4) { it.average() }
    val pairwiseDifferences = items.zipWithNext { a, b -> b - a }

    println("items: $items\n")

    println("chunked into lists: $chunkedIntoLists")
    println("3D points: $points3d")
    println("windowed by 4: $windowed")
    println("sliding average by 4: $slidingAverage")
    println("pairwise differences: $pairwiseDifferences")
}

fill、replaceAll、shuffle/shuffled

新增了一组用于操作列表的扩展函数:用于 MutableList 的 fill、replaceAll 和 shuffle,以及用于只读 List 的 shuffled:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun main(args: Array<String>) {
    val items = (1..5).toMutableList()

    items.shuffle()
    println("Shuffled items: $items")

    items.replaceAll { it * 2 }
    println("Items doubled: $items")

    items.fill(5)
    println("Items filled with 5: $items")
}

kotlin-stdlib 中的数学运算

为满足长期以来的需求,Kotlin 1.2 添加了 JVM 与 JS 通用的数学运算 API kotlin.math,其中包含以下内容:

  • 常量:PI 和 E
  • 三角函数:cos、sin、tan 及其反函数:acos、asin、atan、atan2
  • 双曲函数:cosh、sinh、tanh 及其反函数:acosh、asinh、atanh
  • 指数运算:pow(扩展函数)、sqrt、hypot、exp、expm1
  • 对数:log、log2、log10、ln、ln1p
  • 舍入:
  • ceil、floor、truncate、round(四舍六入五成双)函数
  • roundToInt、roundToLong(四舍五入到整数)扩展函数
  • 符号与绝对值:
  • abs 和 sign 函数
  • absoluteValue 和 sign 扩展属性
  • withSign 扩展函数
  • 两个值的 max 和 min
  • 二进制表示:
  • ulp 扩展属性
  • nextUp、nextDown、nextTowards 扩展函数
  • toBits、toRawBits、Double.fromBits(这些位于 kotlin 包中)

同一组函数(但没有常量)也可用于 Float 实参。

BigInteger 和 BigDecimal 的运算符与转换

Kotlin 1.2 引入了一组函数,用于操作 BigInteger 和 BigDecimal,以及从其他数值类型创建它们。这些函数是:

  • 用于 Int 和 Long 的 toBigInteger
  • 用于 Int、Long、Float、Double 和 BigInteger 的 toBigDecimal
  • 算术和位运算符函数:
  • 二元运算符 +、-、*、/、% 以及中缀函数 and、or、xor、shl、shr
  • 一元运算符 -、++、--,以及函数 inv

浮点数与位表示之间的转换

新增了用于在 Double 和 Float 与其位表示之间转换的函数:

  • toBits 和 toRawBits,对 Double 返回 Long,对 Float 返回 Int
  • Double.fromBits 和 Float.fromBits,用于从位表示创建浮点数

Regex 现在可序列化

kotlin.text.Regex 类已成为 Serializable,现在可以用在可序列化的层次结构中。

Closeable.use 在可用时调用 Throwable.addSuppressed

当在关闭资源的过程中(此前已抛出其他异常时)抛出异常,Closeable.use 函数会调用 Throwable.addSuppressed。

要启用该行为,你的依赖中需要有 kotlin-stdlib-jdk7。

JVM 后端

构造器调用的规范化

从 1.0 版本起,Kotlin 就支持带复杂控制流的表达式,例如 try-catch 表达式和内联函数调用。按照 Java 虚拟机规范,这类代码是合法的。遗憾的是,当这类表达式出现在构造器调用的实参中时,一些字节码处理工具无法很好地处理它们。

为缓解这类字节码处理工具使用者遇到的问题,我们添加了命令行编译器选项(-Xnormalize-constructor-calls=MODE),让编译器为这类构造生成更像 Java 的字节码。这里的 MODE 是以下之一:

  • disable(默认)—— 以与 Kotlin 1.0 和 1.1 相同的方式生成字节码。
  • enable —— 为构造器调用生成类似 Java 的字节码。这可能改变类的加载和初始化顺序。
  • preserve-class-initialization —— 为构造器调用生成类似 Java 的字节码,同时确保保持类的初始化顺序。这可能影响应用的总体性能;只有在多个类之间共享复杂状态并在类初始化时更新它时才使用它。

“手动”的变通办法是把带控制流的子表达式的值存入变量,而不是在调用实参中直接求值。这与 -Xnormalize-constructor-calls=enable 类似。

Java 默认方法调用

在 Kotlin 1.2 之前,以 JVM 1.6 为目标时,覆盖 Java 默认方法的接口成员会在父调用处产生警告:Super calls to Java default methods are deprecated in JVM target 1.6. Recompile with '-jvm-target 1.8'。在 Kotlin 1.2 中,这会改为错误,因此任何此类代码都必须以 JVM 目标 1.8 编译。

破坏性变更:平台类型上 x.equals(null) 行为一致

对映射到 Java 原始类型的平台类型(Int!、Boolean!、Short!、Long!、Float!、Double!、Char!)调用 x.equals(null) 时,如果 x 为 null,此前会错误地返回 true。从 Kotlin 1.2 开始,对平台类型的 null 值调用 x.equals(...) 会抛出 NPE(但 x == ... 不会)。

要恢复 1.2 之前的行为,请向编译器传入标志 -Xno-exception-on-explicit-equals-for-boxed-null。

破坏性变更:修复平台 null 通过内联扩展接收者逃逸的问题

在平台类型的 null 值上调用的内联扩展函数此前不会检查接收者是否为 null,从而允许 null 逃逸到其他代码中。Kotlin 1.2 在调用点强制进行该检查,如果接收者为 null 就抛出异常。

要切换到旧行为,请向编译器传入回退标志 -Xno-receiver-assertions。

JavaScript 后端

默认启用 TypedArrays 支持

用于把 Kotlin 原始数组(例如 IntArray、DoubleArray)转换为 JavaScript 类型化数组的 JS 类型化数组支持此前是可选启用的特性,现在已默认启用。

工具

把警告视为错误

编译器现在提供了把所有警告都视为错误的选项。请在命令行中使用 -Werror,或使用以下 Gradle 代码片段:

1
2
3
compileKotlin {
    kotlinOptions.allWarningsAsErrors = true
}