13.6.13 Kotlin 1.1 的新变化

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

13.6.13 Kotlin 1.1 的新变化

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

发布日期:2016 年 2 月 15 日

目录

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

JavaScript

从 Kotlin 1.1 开始,JavaScript 目标不再被视为实验性的。所有语言特性都受支持,并且有许多用于与前端开发环境集成的新工具。更详细的变更列表请见下文。

协程(实验性)

Kotlin 1.1 的关键新特性是协程,它带来了对 async/await、yield 及类似编程模式的支持。Kotlin 设计的关键特点是:协程执行的实现属于库而不是语言,因此你不受任何特定编程范式或并发库的束缚。

协程实际上是一个轻量级线程,可以被挂起并在之后恢复。协程通过_挂起函数_得到支持:调用这类函数可能会挂起协程,而启动新协程通常使用匿名挂起函数(即挂起 lambda)。

我们来看看在外部库 kotlinx.coroutines 中实现的 async/await:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 在线程池的后台运行该代码
fun asyncOverlay() = async(CommonPool) {
    // 启动两个异步操作
    val original = asyncLoadImage("original")
    val overlay = asyncLoadImage("overlay")
    // 然后把 overlay 应用到两个结果上
    applyOverlay(original.await(), overlay.await())
}

// 在 UI 上下文中启动新的协程
launch(UI) {
    // 等待异步 overlay 完成
    val image = asyncOverlay().await()
    // 然后在 UI 中显示它
    showImage(image)
}

在这里,async { ... } 会启动一个协程;当我们使用 await() 时,协程的执行会被挂起,直到被等待的操作执行完成,然后在被等待的操作完成时恢复(可能在另一个线程上)。

标准库使用协程来支持通过 yield 和 yieldAll 函数实现的惰性生成序列。在这样的序列中,返回序列元素的代码块在每次取出一个元素后被挂起,并在请求下一个元素时恢复。下面是一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import kotlin.coroutines.experimental.*

fun main(args: Array<String>) {
    val seq = buildSequence {
      for (i in 1..5) {
          // 产出 i 的平方
          yield(i * i)
      }
      // 产出一个区间
      yieldAll(26..28)
    }

    // 打印该序列
    println(seq.toList())
}

运行上面的代码看看结果。你可以随意修改它并再次运行!

更多信息请参阅协程文档和教程。

请注意,协程目前被视为实验性特性,这意味着 Kotlin 团队不承诺在 1.1 正式版发布之后继续保持该特性的向后兼容性。

其他语言特性

类型别名

类型别名允许你为现有类型定义另一个名称。这对集合等泛型类型以及函数类型最有用。下面是一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typealias OscarWinners = Map<String, String>

fun countLaLaLand(oscarWinners: OscarWinners) =
        oscarWinners.count { it.value.contains("La La Land") }

// 注意类型名(原始类型和类型别名)可以互换使用:
fun checkLaLaLandIsTheBestMovie(oscarWinners: Map<String, String>) =
        oscarWinners["Best picture"] == "La La Land"

fun oscarWinners(): OscarWinners {
    return mapOf(
            "Best song" to "City of Stars (La La Land)",
            "Best actress" to "Emma Stone (La La Land)",
            "Best picture" to "Moonlight" /* ... */)
}

fun main(args: Array<String>) {
    val oscarWinners = oscarWinners()

    val laLaLandAwards = countLaLaLand(oscarWinners)
    println("LaLaLandAwards = $laLaLandAwards (in our small example), but actually it's 6.")

    val laLaLandIsTheBestMovie = checkLaLaLandIsTheBestMovie(oscarWinners)
    println("LaLaLandIsTheBestMovie = $laLaLandIsTheBestMovie")
}

更多细节请参阅类型别名文档和 KEEP。

有界可调用引用

你现在可以使用 :: 运算符获取指向某个特定对象实例的方法或属性的成员引用。此前这只能用 lambda 表达。下面是一个例子:

1
2
3
4
5
6
val numberRegex = "\\d+".toRegex()
val numbers = listOf("abc", "123", "456").filter(numberRegex::matches)

fun main(args: Array<String>) {
    println("Result is $numbers")
}

更多细节请阅读文档和 KEEP。

密封类和 data 类

Kotlin 1.1 移除了 Kotlin 1.0 中对密封类和 data 类的一些限制。现在你可以在同一个文件的顶层定义顶层密封类的子类,而不仅仅是作为该密封类的嵌套类。data 类现在可以继承其他类。这可以用来清晰简洁地定义表达式类层次结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
sealed class Expr

data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when (expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}
val e = eval(Sum(Const(1.0), Const(2.0)))

fun main(args: Array<String>) {
    println("e is $e") // 3.0
}

更多细节请阅读密封类文档,或关于密封类和 data 类的 KEEP。

lambda 中的解构

你现在可以使用解构声明语法来解包传给 lambda 的实参。下面是一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun main(args: Array<String>) {
    val map = mapOf(1 to "one", 2 to "two")
    // 之前
    println(map.mapValues { entry ->
      val (key, value) = entry
      "$key -> $value!"
    })
    // 现在
    println(map.mapValues { (key, value) -> "$key -> $value!" })
}

更多细节请阅读解构声明文档和 KEEP。

用下划线表示未使用的参数

对于带多个参数的 lambda,你可以用 _ 字符替换不使用的参数名:

1
2
3
4
5
fun main(args: Array<String>) {
    val map = mapOf(1 to "one", 2 to "two")

    map.forEach { _, value -> println("$value!") }
}

这同样适用于解构声明:

1
2
3
4
5
6
7
8
data class Result(val value: Any, val status: String)

fun getResult() = Result(42, "ok").also { println("getResult() returns $it") }

fun main(args: Array<String>) {
    val (_, status) = getResult()
    println("status is '$status'")
}

更多细节请阅读 KEEP。

数值字面量中的下划线

正如 Java 8 一样,Kotlin 现在允许在数值字面量中使用下划线来分隔数字分组:

1
2
3
4
5
6
7
8
9
val oneMillion = 1_000_000
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

fun main(args: Array<String>) {
    println(oneMillion)
    println(hexBytes.toString(16))
    println(bytes.toString(2))
}

更多细节请阅读 KEEP。

更短的属性语法

对于 getter 定义为表达式体的属性,现在可以省略属性类型:

1
2
3
4
5
6
7
    data class Person(val name: String, val age: Int) {
    val isAdult get() = age >= 20 // 属性类型被推断为 'Boolean'
}
fun main(args: Array<String>) {
    val akari = Person("Akari", 26)
    println("$akari.isAdult = ${akari.isAdult}")
}

内联属性访问器

如果属性没有幕后字段,你现在可以用 inline 修饰符标记属性访问器。这类访问器的编译方式与内联函数相同。

1
2
3
4
5
6
7
8
public val <T> List<T>.lastIndex: Int
    inline get() = this.size - 1

fun main(args: Array<String>) {
    val list = listOf('a', 'b')
    // getter 会被内联
    println("Last index of $list is ${list.lastIndex}")
}

你也可以把整个属性标记为 inline —— 那样该修饰符会应用到两个访问器上。

更多细节请阅读内联函数文档和 KEEP。

局部委托属性

你现在可以对局部变量使用委托属性语法。一种可能的用法是定义延迟求值的局部变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import java.util.Random

fun needAnswer() = Random().nextBoolean()

fun main(args: Array<String>) {
    val answer by lazy {
        println("Calculating the answer...")
        42
    }
    if (needAnswer()) { // 返回随机值
        println("The answer is $answer.") // answer 此时被计算
    }
    else {
        println("Sometimes no answer is the answer...")
    }
}

更多细节请阅读 KEEP。

拦截委托属性绑定

对于委托属性,现在可以通过 provideDelegate 运算符拦截委托与其属性之间的绑定。例如,如果我们想在绑定前检查属性名,可以这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(thisRef: MyUI, prop: KProperty<*>): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        ... // 属性创建
    }

    private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

class MyUI {
    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

在创建 MyUI 实例的过程中,每个属性都会调用 provideDelegate 方法,它可以立即执行必要的校验。

更多细节请阅读委托属性文档。

泛型的枚举值访问

现在可以以泛型方式枚举枚举类的值。

1
2
3
4
5
6
7
8
9
enum class RGB { RED, GREEN, BLUE }

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

fun main(args: Array<String>) {
    printAllValues<RGB>() // 打印 RED, GREEN, BLUE
}

DSL 中隐式接收者的作用域控制

@DslMarker 注解允许在 DSL 上下文中限制对外层作用域接收者的使用。考虑经典的 HTML 构建器示例:

1
2
3
4
5
table {
    tr {
        td { + "Text" }
    }
}

在 Kotlin 1.0 中,传给 td 的 lambda 中的代码可以访问三个隐式接收者:传给 table 的、传给 tr 的和传给 td 的。这让你可以调用在该上下文中毫无意义的方法,例如在 td 内部调用 tr,从而把 标签放进 中。

在 Kotlin 1.1 中,你可以限制这一点,使传给 td 的 lambda 内部只能使用定义在 td 隐式接收者上的方法。为此,你需要定义一个用 @DslMarker 元注解标记的注解,并把它应用到标签类的基类上。

更多细节请阅读类型安全构建器文档和 KEEP。

rem 运算符

mod 运算符现在已被弃用,改用 rem。关于其动机,请参阅这个 issue。

标准库

字符串到数值的转换

String 类上新增了一批扩展,可以在数字无效时不抛异常地把它转换为数值:String.toIntOrNull(): Int?、String.toDoubleOrNull(): Double? 等等。

1
val port = System.getenv("PORT")?.toIntOrNull() ?: 80

此外,整数转换函数(例如 Int.toString()、String.toInt()、String.toIntOrNull())各自都新增了带 radix 参数的重载,用来指定转换的进制(2 到 36)。

onEach()

onEach 是一个小巧但实用的集合与序列扩展函数,它允许在操作链中对集合/序列的每个元素执行某些操作(可能带有副作用)。在可迭代对象上,它的行为类似 forEach,但还会把该可迭代对象继续返回下去。在序列上,它返回一个包装序列,在迭代元素时惰性地应用给定操作。

1
2
3
4
inputDir.walk()
        .filter { it.isFile && it.name.endsWith(".txt") }
        .onEach { println("Moving $it to $outputDir") }
        .forEach { moveFile(it, File(outputDir, it.toRelativeString(inputDir))) }

also()、takeIf() 和 takeUnless()

这是三个适用于任何接收者的通用扩展函数。

also 类似 apply:它接收接收者、对其执行某些操作,然后返回该接收者。区别在于 apply 块内的接收者以 this 形式可用,而 also 块内以 it 形式可用(你也可以给它取别的名字)。当你不希望遮蔽外层的 this 时,这就很方便:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Block {
    lateinit var content: String
}

fun Block.copy() = Block().also {
    it.content = this.content
}

// 改用 'apply'
fun Block.copy1() = Block().apply {
    this.content = this@copy1.content
}

fun main(args: Array<String>) {
    val block = Block().apply { content = "content" }
    val copy = block.copy()
    println("Testing the content was copied:")
    println(block.content == copy.content)
}

takeIf 就像针对单个值的 filter。它检查接收者是否满足谓词,满足则返回接收者,不满足则返回 null。结合 Elvis 运算符(?:)和提前 return,可以写出这样的结构:

1
2
val outDirFile = File(outputDir.path).takeIf { it.exists() } ?: return false
// 对已存在的 outDirFile 做一些处理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main(args: Array<String>) {
    val input = "Kotlin"
    val keyword = "in"

    val index = input.indexOf(keyword).takeIf { it >= 0 } ?: error("keyword not found")
    // 既然已找到,就可以用关键字在输入字符串中的索引做些什么

    println("'$keyword' was found in '$input'")
    println(input)
    println(" ".repeat(index) + "^")
}

takeUnless 与 takeIf 相同,但接受取反的谓词。当接收者_不_满足谓词时返回接收者,否则返回 null。因此上面某个例子可以用 takeUnless 改写如下:

1
val index = input.indexOf(keyword).takeUnless { it < 0 } ?: error("keyword not found")

当你持有可调用引用而不是 lambda 时,使用它也很方便:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private fun testTakeUnless(string: String) {
    val result = string.takeUnless(String::isEmpty)

    println("string = \"$string\"; result = \"$result\"")
}

fun main(args: Array<String>) {
    testTakeUnless("")
    testTakeUnless("abc")
}

groupingBy()

这个 API 可以按键对集合分组,并同时对每个分组进行折叠。例如,它可以用统计以每个字母开头的单词数量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun main(args: Array<String>) {
    val words = "one two three four five six seven eight nine ten".split(' ')
    val frequencies = words.groupingBy { it.first() }.eachCount()
    println("Counting first letters: $frequencies.")

    // 使用 'groupBy' 和 'mapValues' 的替代方式会创建中间映射,
    // 而 'groupingBy' 方式则即时计数。
    val groupBy = words.groupBy { it.first() }.mapValues { (_, list) -> list.size }
    println("Comparing the result with using 'groupBy': ${groupBy == frequencies}.")
}

Map.toMap() 与 Map.toMutableMap()

这些函数可用于轻松复制映射:

1
2
3
class ImmutablePropertyBag(map: Map<String, Any>) {
    private val mapCopy = map.toMap()
}

Map.minus(key)

运算符 plus 提供了一种向只读映射添加键值对并生成新映射的方式,但此前没有简单的方法做相反的操作:要从映射中移除一个键,你必须求助于 Map.filter() 或 Map.filterKeys() 这类不那么直接的方式。现在 minus 运算符填补了这一空白。它有 4 个重载:移除单个键、一组键、一个键的序列,以及一个键的数组。

1
2
3
4
5
6
7
fun main(args: Array<String>) {
    val map = mapOf("key" to 42)
    val emptyMap = map - "key"

    println("map: $map")
    println("emptyMap: $emptyMap")
}

minOf() 与 maxOf()

这些函数可用于找出两个或三个给定值中的最小值和最大值,这些值可以是原始数值或 Comparable 对象。如果你想比较本身不可比较的对象,每个函数还有一个接收额外 Comparator 实例的重载。

1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
    val list1 = listOf("a", "b")
    val list2 = listOf("x", "y", "z")
    val minSize = minOf(list1.size, list2.size)
    val longestList = maxOf(list1, list2, compareBy { it.size })

    println("minSize = $minSize")
    println("longestList = $longestList")
}

类似数组的 List 实例化函数

与 Array 构造器类似,现在有了一些函数,可以创建 List 和 MutableList 实例,并通过调用 lambda 初始化每个元素:

1
2
3
4
5
6
7
fun main(args: Array<String>) {
    val squares = List(10) { index -> index * index }
    val mutable = MutableList(10) { 0 }

    println("squares: $squares")
    println("mutable: $mutable")
}

Map.getValue()

这个 Map 上的扩展返回与给定键对应的现有值,或抛出异常并指出哪个键未找到。如果该映射是通过 withDefault 生成的,这个函数会返回默认值而不是抛出异常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fun main(args: Array<String>) {
    val map = mapOf("key" to 42)
    // 返回不可为空的 Int 值 42
    val value: Int = map.getValue("key")

    val mapWithDefault = map.withDefault { k -> k.length }
    // 返回 4
    val value2 = mapWithDefault.getValue("key2")

    // map.getValue("anotherKey") // <- 这会抛出 NoSuchElementException

    println("value is $value")
    println("value2 is $value2")
}

抽象集合

这些抽象类可以在实现 Kotlin 集合类时用作基类。实现只读集合时有 AbstractCollection、AbstractList、AbstractSet 和 AbstractMap;实现可变集合时有 AbstractMutableCollection、AbstractMutableList、AbstractMutableSet 和 AbstractMutableMap。在 JVM 上,这些抽象可变集合的大部分功能都继承自 JDK 的抽象集合。

数组操作函数

标准库现在提供了一组逐元素操作数组的函数:比较(contentEquals 和 contentDeepEquals)、计算哈希码(contentHashCode 和 contentDeepHashCode)以及转换为字符串(contentToString 和 contentDeepToString)。它们在 JVM(作为 java.util.Arrays 中对应函数的别名)和 JS(实现位于 Kotlin 标准库中)上都受支持。

1
2
3
4
5
fun main(args: Array<String>) {
    val array = arrayOf("a", "b", "c")
    println(array.toString()) // JVM 实现:类型名加哈希的乱码
    println(array.contentToString()) // 格式化为漂亮的列表
}

JVM 后端

Java 8 字节码支持

Kotlin 现在可以选择生成 Java 8 字节码(命令行选项 -jvm-target 1.8,或 Maven/Gradle 中的对应选项)。目前这不会改变字节码的语义(特别是接口中的默认方法和 lambda 的生成方式与 Kotlin 1.0 完全相同),但我们计划稍后进一步利用它。

Java 8 标准库支持

现在有单独的、支持 Java 7 和 8 中新增 JDK API 的标准库版本。如果你需要访问这些新 API,请使用 kotlin-stdlib-jre7 和 kotlin-stdlib-jre8 Maven 产物而不是标准的 kotlin-stdlib。这些产物是构建在 kotlin-stdlib 之上的小型扩展,会把它作为传递依赖引入你的项目。

字节码中的参数名

Kotlin 现在支持把参数名存储在字节码中。可以使用 -java-parameters 命令行选项启用。

常量内联

编译器现在会把 const val 属性的值内联到使用它们的位置。

可变的闭包变量

用于在 lambda 中捕获可变闭包变量的装箱类不再具有 volatile 字段。这一变化提升了性能,但在少数使用场景中可能引入新的竞态条件。如果你受影响,需要自己为访问这些变量提供同步。

javax.script 支持

Kotlin 现在与 javax.script API(JSR-223)集成。该 API 允许在运行时求值代码片段:

1
2
3
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
engine.eval("val x = 3")
println(engine.eval("x + 2")) // 输出 5

使用该 API 的更完整示例项目请见这里。

kotlin.reflect.full

为了为支持 Java 9 做准备,kotlin-reflect.jar 库中的扩展函数和属性已被移到 kotlin.reflect.full 包中。旧包(kotlin.reflect)中的名称已被弃用,并将在 Kotlin 1.2 中移除。请注意,核心反射接口(例如 KClass)属于 Kotlin 标准库而不是 kotlin-reflect,不受此次移动影响。

JavaScript 后端

统一的标准库

现在可以从编译为 JavaScript 的代码中使用更大一部分 Kotlin 标准库。特别是集合(ArrayList、HashMap 等)、异常(IllegalArgumentException 等)以及其他一些类(StringBuilder、Comparator)现在都定义在 kotlin 包下。在 JVM 上,这些名称是指向相应 JDK 类的类型别名;在 JS 上,这些类在 Kotlin 标准库中实现。

更好的代码生成

JavaScript 后端现在生成更多可静态检查的代码,这对压缩器、优化器、linter 等 JS 代码处理工具更友好。

external 修饰符

如果你需要以类型安全的方式从 Kotlin 访问某个用 JavaScript 实现的类,可以使用 external 修饰符编写 Kotlin 声明。(在 Kotlin 1.0 中使用的是 @native 注解。)与 JVM 目标不同,JS 目标允许对类和属性使用 external 修饰符。例如,你可以这样声明 DOM 的 Node 类:

1
2
3
4
5
6
7
8
9
external class Node {
    val firstChild: Node

    fun appendChild(child: Node): Node

    fun removeChild(child: Node): Node

    // 等等
}

改进的 import 处理

你现在可以更精确地描述应从 JavaScript 模块导入的声明。如果在外部声明上添加 @JsModule("<module-name>") 注解,它会在编译期间被正确导入到某个模块系统(CommonJS 或 AMD)中。例如在 CommonJS 中,该声明会通过 require(...) 函数导入。此外,如果你想把某个声明既作为模块又作为全局 JavaScript 对象导入,可以使用 @JsNonModule 注解。

例如,你可以这样把 JQuery 导入 Kotlin 模块:

1
2
3
4
5
6
7
8
9
external interface JQuery {
    fun toggle(duration: Int = definedExternally): JQuery
    fun click(handler: (Event) -> Unit): JQuery
}

@JsModule("jquery")
@JsNonModule
@JsName("$")
external fun jquery(selector: String): JQuery

在这种情况下,JQuery 会作为一个名为 jquery 的模块导入。或者,根据 Kotlin 编译器所配置使用的模块系统,它也可以作为 $ 对象使用。

你可以在应用中这样使用这些声明:

1
2
3
4
5
fun main(args: Array<String>) {
    jquery(".toggle-button").click {
        jquery(".toggle-panel").toggle(300)
    }
}