Kotlin的自动类型转换
Kotlin 1.4 正式版在好早以前就已经发布了。关于那些“看得见”的新特性,比如SAM转换、参数列表末尾的可选逗号什么的,已经有无数文章介绍过了。所以本文打算介绍一些可能是鲜为人知的、Kotlin 官方团队偷偷塞进 1.4 的新特性。
不过单独讲这些东西会显得文章太过单薄,于是我打算把其他相似的东西拉一起凑凑字数。
本文使用的 Kotlin 版本为 Kotlin 1.4。
本文要讲的东西
看题目就知道了,Kotlin 里自动类型转换(automatic type conversion)。这里讲的不是 「把一个 String
转成 Any
,再转成 String
」 这种和子类型有关的东西,当然也不是 Smart Cast,而是两个不相容的类型之间的转换,比如说 Int
转成 Long
,如下文所示。
数值转换
一般地,在 Kotlin 里我们不能像 Java 一样直接把一个 Int
类型的东西赋值给 Long
类型的变量,因为它们之间并不具有子类型关系。像下面这样会得到一个编译错误:
val int: Int = 555
val long: Long = int // 编译错误!
println(long)
你需要调用标准库提供给你的那些 toXXX
函数把数值转换成其他类型的数值。
val int: Int = 555
val long: Long = int.toLong() // OK
println(long)
Kotlin 官方团队曾经表示过不喜欢隐式(implicit)的东西,关于数值的隐式类型转换也包括在内。这就导致了使用 Kotlin 在进行一些关于数值方面的操作时,有时候会写出一些看起来无比蛋疼的代码。
Bennyhuo:就是有时候写点儿计算比较多的代码,满篇的 toFloat toDouble。
不一般地,我们可以使用 @Suppress
来搞事:
val int: Int = 233
@Suppress("TYPE_MISMATCH")
val long: Long = int
println(long) // 233
这个代码是可以跑起来的,而且你真的可以从字节码里看到那个把 Int
转成 Long
的指令 I2L
。
不过我不确定 Kotlin 的其他 target 是否能这样用,我也不保证这样写完全不会出问题。(这里是关于 @Suppress
的免责声明,请读者自行脑补)
SAM Conversion
SAM 转换也是一种自动类型转换。它把一个 lambda 表达式(具有函数类型)转成某个具体的接口类型。
fun interface ISome {
fun some()
}
fun useSome(some: ISome) {}
useSome { println("some") }
如果读者不同意这个说法,可以选择跳过本小节内容。
在我的另一篇文章里有更详细的介绍。
Coercion to Unit
我们都知道 Kotlin 的 lambda 表达式是使用里面最后一个表达式的值来作为 lambda 的返回值的。比如这样:
val block = { "yeah" }
println(block.invoke()) // 输出 yeah
block 的类型是 () -> String
。
然后我们来看看这样的情况:
fun test(block: () -> Unit) {
println(block())
}
test { "yeah" } // 输出 kotlin.Unit
我们可以注意到那个 block 的类型是 () -> Unit
,相信很多人都熟悉这样的写法。
在某些初学者的眼里这看起来像是把一个 () -> String
类型的 lambda 传给了需要 () -> Unit
类型的函数。
这就是 coercion to unit,一个很久以前就存在的特性,可以理解为编译器自动帮你在 lambda 表达式的最后加了一行 Unit
,把本来应该是() -> String
类型的 lambda 变成了 () -> Unit
类型。
在 Kotlin 1.4 版本,这个特性得到了进化,你甚至可以这样写:
fun test(block: () -> Unit) {
println(block())
}
fun some(): String {
return "str"
}
// 需要 Kotlin 1.4 版本
test(::some) // 输出 kotlin.Unit
编译器帮你把 () -> String
类型的函数引用转成了 () -> Unit
。
Unit Conversion
警告:这是一项未完成的特性!
添加编译器参数 -XXLanguage:+UnitConversion
,你就开启了一个 Kotlin 官方团队偷偷塞进 1.4 版本的未完成的新特性。
这个特性允许你写出这样的代码:
fun test(block: () -> Unit) {
println(block())
}
fun some(block: () -> String) {
test(block) // 这里是重点
// 如果你不加那个编译器参数,会报错
}
fun main() {
some { "str" }
// 理论上会输出 kotlin.Unit
}
在函数 some 里把一个 () -> String
传给了 test 函数,可以看出来这个特性其实和 coercion to unit 是差不多的。
理论上这样的代码运行时会输出 kotlin.Unit
,但是目前由于该特性的代码生成没写好,得不到预期的结果。
另外,在开启了这个特性后,() -> String
并不会成为 () -> Unit
的子类型,它们依然是两个不相容的类型。这个转换只能在调用函数时发生。
fun test(block: () -> Unit) {
println(block())
}
val a: () -> String = { "" }
val b: () -> Unit = a // 错误!
test(a) // unit conversion只会在这种情况下发生!
Suspend Conversion
警告:这是一项未完成的特性!
这是本文要介绍的第二个 Kt 官方团队偷偷塞进 1.4 版本的未完成的新特性。
比如说我们有这样的一个函数:
fun test(f: suspend () -> Unit) {
// do something with f
}
我们可以这样调用它:
// case 1
test { println("hi") }
但是这样不行:
// case 2
val f = { println("hi") }
test(f) // 编译错误
编译器会告诉你类型不匹配,f
是 () -> Unit
类型,test
函数需要 suspend () -> Unit
类型的参数。
为什么呢?
在 case 1 里,lambda 字面量是直接传给 test 函数的,Kotlin 编译器会根据 test 函数的参数类型将这个 lambda 字面量当作是 suspend lambda 来看待,即 suspend () -> Unit
。
在 case 2 里,编译器无法从 val f = { println("hi") }
这行语句中获得关于 lambda 字面量的类型的信息,所以就默认推断为普通的函数类型,即 () -> Unit
。(对于Kotlin来说,下一行的函数调用并不会影响到 f 的类型推断)
当你添加了编译器参数 -XXLanguage:+SuspendConversion
,就可以让 case 2 通过编译。
也就是说这个特性可以帮你把普通函数类型的值转成 suspend 函数类型。
和上面的 unit conversion 差不多,这个 suspend conversion 也是只有在调用函数的时候才发生的。
fun test(block: suspend () -> String) {
}
val a: () -> String = { "" }
val b: suspend () -> String = a // 错误!
test(a) // suspend conversion只会在这种情况下发生!
当然由于这是未完成的功能,即使上面的 case 2 可以通过编译,但是跑起来还是会炸。
这个特性或许会在 Kotlin 1.5 版本完工,但请不要抱有期待。
结尾
我并不想讨论「为什么要加这种奇怪的特性」之类的话题。
不可否认的是,在有限的程序员生涯中,这些新特性可能一次也用不上。上面提到的问题也都有相应的 workaround,不需要新特性也可以写出等价的代码,就是没有那么优雅罢了(
本文完。
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。
本文链接:https://aisia.moe/2020/09/12/kotlin-auto-type-conversion/