Kotlin 1.2 新特性
发布日期:2017 年 11 月 28 日
目录
多平台项目 (实验性的)
多平台项目是 Kotlin 1.2 中一项新的实验性的特性,它允许你在 Kotlin 支持的目标平台(JVM、JavaScript 和(未来会支持的)Native)之间复用代码。在多平台项目中,你有三种类型的模块:
- 公共模块包含不特定于任何平台的代码,以及没有实现依赖于平台的 API 的声明。
- 平台模块包含公共模块中依赖于平台的声明针对特定平台的实现,以及其他依赖于平台的代码。
- 常规模块面向特定平台,可以作为平台模块的依赖项,也可以依赖于平台模块。
当你针对特定平台编译多平台项目时,公共模块和平台特有部分的代码都会生成。
多平台项目支持的一个关键特性是,能够通过 expected 和 actual 声明来表达公共代码对平台特有部分的依赖项。expected 声明指定了一个 API(类、接口、注解、顶层****声明等)。actual 声明要么是 API 的依赖于平台的实现,要么是引用外部库中 API 现有实现的类型别名。以下是一个示例:
在公共代码中:
// 预期的平台特有 API:
expect fun hello(world: String): String
fun greet() {
// 预期的 API 用法:
val greeting = hello("multiplatform world")
println(greeting)
}
expect class URL(spec: String) {
open fun getHost(): String
open fun getPath(): String
}在 JVM 平台代码中:
actual fun hello(world: String): String =
"Hello, $world, on the JVM platform!"
// 使用现有平台特有实现:
actual typealias URL = java.net.URL关于详细信息和构建多平台项目的步骤,请参见多平台编程文档。
其他语言特性
注解中的数组字面值
从 Kotlin 1.2 开始,注解的数组****实参可以通过新的数组字面值语法传递,而不是使用 arrayOf 函数:
@CacheConfig(cacheNames = ["books", "default"])
public class BookRepositoryImpl {
// ...
}数组字面值语法受限于注解****实参。
lateinit 顶层属性和局部变量
现在,lateinit 修饰符可以用于顶层属性和局部变量。例如,后者可以用于当作为构造函数****实参传递给一个对象的 lambda 表达式引用必须在之后定义的另一个对象时:
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 变量是否已初始化:
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()
}带默认函数形参的内联函数
现在内联函数允许为其内联的函数****形参设置默认值:
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 调用:
val button = findViewById(R.id.button) as Button智能类型转换改进
当变量从安全调用表达式赋值并检测是否为 null 时,智能类型转换现在也会应用于安全调用接收者:
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 表达式之前被修改的局部变量:
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 中被弃用。
弃用:数据类覆盖 copy
当数据类派生自一个已经拥有同名同签名 copy 函数的类型时,为该数据类生成的 copy 实现会使用超类中的默认值,这会导致反直觉的行为,或者在超类中没有默认****形参时在运行时****失败。
导致 copy 冲突的继承在 Kotlin 1.2 中已发出警告并被弃用,在 Kotlin 1.3 中将成为错误。
弃用:枚举项中的嵌套类型
在枚举项内部,定义非 inner class 的嵌套类型因初始化逻辑中的问题已被弃用。这会在 Kotlin 1.2 中导致警告,并在 Kotlin 1.3 中成为错误。
弃用:vararg 的单一命名实参
为了与注解中的数组字面值保持一致,以命名形式 (foo(items = i)) 为 vararg 形参传递单个项已被弃用。请使用展开操作符和相应的数组****工厂函数:
foo(items = *arrayOf(1))存在一项优化,可以消除此类情况下冗余的数组****创建,从而防止性能下降。单实参形式在 Kotlin 1.2 中会产生警告,并将在 Kotlin 1.3 中被移除。
弃用:继承 Throwable 的泛型类的内部类
继承自 Throwable 的泛型类型的内部类可能在 throw-catch 场景中违反类型安全,因此已被弃用,在 Kotlin 1.2 中发出警告,并在 Kotlin 1.3 中成为错误。
弃用:修改****只读属性的幕后字段
在自定义 getter 中通过赋值 field = ... 修改****只读属性的幕后字段已被弃用,在 Kotlin 1.2 中发出警告,并在 Kotlin 1.3 中成为错误。
标准库
Kotlin 标准库 artifact 和拆分****包
Kotlin 标准库现在与 Java 9 模块系统完全兼容,后者禁止拆分****包(多个 jar 文件在同一个包中声明****类)。为了支持这一点,引入了新的 artifact kotlin-stdlib-jdk7 和 kotlin-stdlib-jdk8,它们替换了旧的 kotlin-stdlib-jre7 和 kotlin-stdlib-jre8。
从 Kotlin 的角度来看,新 artifact 中的声明在相同的包名下可见,但对于 Java 来说包名不同。因此,切换到新的 artifact 不会对你的源代码造成任何更改。
为了确保与新的模块系统兼容而进行的另一个更改是,从 kotlin-reflect 库中移除了 kotlin.reflect 包中已弃用的声明。如果你一直在使用它们,你需要切换到使用在 kotlin.reflect.full 包中的声明,这在 Kotlin 1.1 中已得到支持。
windowed, chunked, zipWithNext
针对 Iterable<T>、Sequence<T> 和 CharSequence 的新扩展涵盖了诸如缓冲或批处理 (chunked)、滑动窗口和计算****滑动平均值 (windowed) 以及处理连续项的对 (zipWithNext) 之类的使用场景:
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
")
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
为操作 list 添加了一组扩展****函数:针对 MutableList 的 fill、replaceAll 和 shuffle,以及针对只读 List 的 shuffled:
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 通用的用于数学运算的 kotlin.math API,并包含以下内容:
- 常量:
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 转换为它们的位表示以及从它们的位表示****转换:
- 针对
Double返回Long、针对Float返回Int的toBits和toRawBits - 用于从位表示创建浮点数的
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 之前,接口****成员在覆盖 Java 默认方法同时面向 JVM 1.6 目标平台时,在 super 调用上会产生警告: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 支持****默认启用
JS 类型化数组支持,它将 Kotlin 原生类型****数组(例如 IntArray、DoubleArray)转换为 JavaScript 类型化****数组,这项之前是选择性****特性的功能已默认启用。
工具
警告视为错误
编译器现在提供一个选项,可以将所有警告视为错误。在命令行中使用 -Werror,或使用以下 Gradle 代码片段:
compileKotlin {
kotlinOptions.allWarningsAsErrors = true
}