Kotlin的自动类型转换

Author Avatar
Dexlind 9月 12, 2020
  • 在其它设备中阅读本文章

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/