类型检查和转换:’is’ 和 ‘as’

is!is 操作符

在运行时可以通过 is 或者它的否定形 !is 来判断一个对象是否是某个类型:

  1. if (obj is String) {
  2. print(obj.length)
  3. }
  4. if (obj !is String) { // same as !(obj is String)
  5. print("Not a String")
  6. } else {
  7. print(obj.length)
  8. }

智能转换

大多数情况下,我们不会用到显示转换操作符,因为编译器会追踪 is 检查以及不可变值的显示转换,然后在需要时自动插入(安全)转换代码:

  1. fun demo(x: Any) {
  2. if (x is String) {
  3. print(x.length) // x is automatically cast to String
  4. }
  5. }

编译器对于类型转换是否安全是足够智能的,例如否定检查导致的返回:

  1. if (x !is String) return
  2. print(x.length) // x is automatically cast to String

或者位于 &&|| 的右侧:

  1. // x is automatically cast to string on the right-hand side of `||`
  2. if (x !is String || x.length == 0) return
  3. // x is automatically cast to string on the right-hand side of `&&`
  4. if (x is String && x.length > 0) {
  5. print(x.length) // x is automatically cast to String
  6. }

这种智能转换也适用于 when 表达式和 while 循环:

  1. when (x) {
  2. is Int -> print(x + 1)
  3. is String -> print(x.length + 1)
  4. is IntArray -> print(x.sum())
  5. }

当编译器无法保证变量在检查和使用之间不能改变时,智能转换并不会工作。更具体一点,只有满足如下条件时,只能转换才可用:

  • val 局部变量 - 永远不会包含局部代理属性。
  • val 属性 - 如果属性是私有属性或内部属性,或者检查发生在属性声明所在的模块。智能转换对于开放的属性或者有自定义 getter 的属性并不可用。
  • var 局部变量 - 这个局部变量不能在检查和使用之间被修改,不能在 lambda 中被修改,也不是局部代理属性。
  • var 属性 - 永远不会(因为这个变量随时都会被其他代码修改)。

“不安全”的转换操作符

通常情况下,如果转换是不可能的,转换操作符会抛出一个异常。我们称之为不安全的。这个不安全的转换在 Kotlin 中是通过中缀操作符 as 完成的:

  1. val x: String = y as String

注意,null 不能转换成 String,因为它是非空类型,例如,如果 ynull,上面的代码会抛出一个异常。为了匹配 Java 的转换语义,我们必须在转换操作的右侧使用可空类型:

  1. val x: String? = y as String?

“安全”(可空)的转换操作

为了避免异常抛出,可以使用安全的转换操作符 as?,它在失败会返回 null

  1. val x: String? = y as? String

注意,即使 as? 右侧是一个非空类型 String,转换的结果依然是可空的。

类型擦除和泛型类型检查

Kotlin 只能在编译时保证涉及到泛型的操作的类型安全,然而在运行时,泛型实例并没有携带关于真实类型实参的任何信息。例如,List<Foo> 会擦除为 List<*>。一般情况下,无法在运行时去检查一个实例是否属于某个具体类型实参的泛型类型。

基于以上,编译器会禁止 is 检查,因为类型擦除的原因,这个检查无法在运行时执行,例如 ints is List<Int> 或者 list is T(类型形参)。但是,我们可以检查一个实例是否是星映射类型。

  1. if (something is List<*>) {
  2. something.forEach { println(it) } // The items are typed as `Any?`
  3. }

同样的,当一个实例的类型参数(在编译时)已经被检查过之后,可以对类型的非泛型部分做 is 检查或者转换。这种情况下会去掉尖括号:

  1. fun handleStrings(list: List<String>) {
  2. if (list is ArrayList) {
  3. // `list` is smart-cast to `ArrayList<String>`
  4. }
  5. }

同样的去掉类型实参的语法也适用于不考虑类型实参的转换:list as ArrayList

带具体化类型参数的内联函数会把实际的类型实参内联到调用处,这也使得 arg is T 对类型参数是可行的,但是,如果 arg 本身是一个泛型的实例,那么它的类型参数依然会被擦除。例如:

  1. inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
  2. if (first !is A || second !is B) return null
  3. return first as A to second as B
  4. }
  5. val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)
  6. val stringToSomething = somePair.asPairOf<String, Any>()
  7. val stringToInt = somePair.asPairOf<String, Int>()
  8. val stringToList = somePair.asPairOf<String, List<*>>()
  9. val stringToStringList = somePair.asPairOf<String, List<String>>()

未检查的转换

如上所述,类型擦除使得在运行时检查泛型实例的实际类型形参变得不可能,而且代码中的泛型类型由于彼此连接得不太紧密,编译器也无法确保类型安全。

即便如此,我们可以在高级程序逻辑里隐含类型安全来作为替代方案。

  1. fun readDictionary(file: File): Map<String, *> =
  2. file.inputStream().use {
  3. TODO("Read a mapping of strings to arbitrary elements.")
  4. }
  5. // We saved a map with `Int`s into that file
  6. val insFile = File("ints.dictionary")
  7. // Warning: Unchecked cast: `Map<String, *>` to `Map<String, Int>`
  8. val intsDictionary: Map<String, Int> =
  9. readDictionary(intsFile) as Map<String, Int>

编译器会对代码最后一行发出警告。类型转换无法在运行时做检查而且不能保证 map 的值是 Int

为了避免未检查的转换,我们可以重新设计程序结构:在上面的例子中,可以使用接口 DictionaryReader<T>DictionaryWriter<T>,它们可以实现不同类型的类型安全。也可以基于合理的抽象把未检查的转换从调用代码的形式转移到实现细节中。泛型变形的合理使用会有所帮助。

对于泛型函数来说,使用具体化的类型参数能够使 arg as T 这样的转换在运行时被检查,除非 arg 的类型有它自己的类型参数(会被擦除)。

如果要取消对未检查的转化所发出的警告,可以把 @Suppress("UNCHECKED_CAST") 标记在声明或表达式上。

  1. inline fun <reified T> List<*>.asListOfType():
  2. List<T>? =
  3. if (all { it is T })
  4. @Suppress("UNCHECKED_CAST")
  5. this as List<T> else
  6. null

在 JVM 中,数组类型(Array<Foo>)会保持被擦除的元素类型的信息,变到数组类型的转化也会有部分检查:可空性,元素类型本身的时机类型实参任然会被擦除。例如,对于 foo as Array<List<String>?> 这种转换,如果 foo 是一个携带了任意 List<*> 的数组,无论是否为空,转换都会成功。