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)
}
}
|