5.4.8 类型检查与类型转换

原文链接: https://kotlinlang.org/docs/typecasts.html

5.4.8 类型检查与类型转换

在 Kotlin 中,你可以在运行时对类型做两件事:检查某个对象是否为特定类型,或者把它转换为另一种类型。类型检查用于确认你正在处理的对象属于哪种类型,而类型转换则尝试把对象变为另一种类型。

提示: 如果你想专门了解泛型的类型检查与转换,例如 List<T>、Map<K,V>,请参阅泛型的类型检查与转换。

使用 is 和 !is 运算符进行检查

使用 is 运算符(或用 !is 取反)在运行时检查对象是否匹配某个类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fun main() {
    val input: Any = "Hello, Kotlin"

    if (input is String) {
        println("Message length: ${input.length}")
        // 消息长度:13
    }

    if (input !is String) { // 等同于 !(input is String)
        println("Input is not a valid message")
    } else {
        println("Processing message: ${input.length} characters")
        // 处理消息:13 个字符
    }
}

你也可以用 is 和 !is 运算符检查对象是否匹配某个子类型:

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
interface Animal {
    val name: String
    fun speak()
}

class Dog(override val name: String) : Animal {
    override fun speak() = println("$name says: Woof!")
}

class Cat(override val name: String) : Animal {
    override fun speak() = println("$name says: Meow!")
}
fun handleAnimal(animal: Animal) {
    println("Handling animal: ${animal.name}")
    animal.speak()

    // 使用 is 运算符检查子类型
    if (animal is Dog) {
        println("Special care instructions: This is a dog.")
    } else if (animal is Cat) {
        println("Special care instructions: This is a cat.")
    }
}
fun main() {
    val pets: List<Animal> = listOf(
        Dog("Buddy"),
        Cat("Whiskers"),
        Dog("Rex")
    )

    for (pet in pets) {
        handleAnimal(pet)
        println("---")
    }
    // 正在照料动物:Buddy
    // Buddy 说:汪!
    // 特别照料说明:这是一只狗。
    // ---
    // 正在照料动物:Whiskers
    // Whiskers 说:喵!
    // 特别照料说明:这是一只猫。
    // ---
    // 正在照料动物:Rex
    // Rex 说:汪!
    // 特别照料说明:这是一只狗。
    // ---
}

这个例子使用 is 运算符检查 Animal 类的实例是 Dog 还是 Cat 子类型,从而打印相应的照料说明。

你可以检查某个对象是否为其声明类型的超类型,但这样做没有意义,因为答案总是 true。每个类的实例本身就已经是其超类型的实例。

要在运行时识别对象的类型,请参阅反射。

类型转换

在 Kotlin 中,把对象的类型转换为另一种类型称为类型转换(casting)。

在某些情况下,编译器会自动为你转换对象,这称为智能转换。

如果需要显式转换类型,请使用 as? 或 as 转换运算符。

智能转换

对于不可变值,编译器会跟踪类型检查和显式转换,并自动插入隐式(安全的)转换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun logMessage(data: Any) {
    // data 被自动转换为 String
    if (data is String) {
        println("Received text: ${data.length} characters")
    }
}

fun main() {
    logMessage("Server started")
    // 收到的文本:14 个字符
    logMessage(404)
}

编译器甚至足够聪明,能够识别出否定检查会导致提前返回时该转换是安全的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun logMessage(data: Any) {
    // data 被自动转换为 String
    if (data !is String) return

    println("Received text: ${data.length} characters")
}

fun main() {
    logMessage("User signed in")
    // 收到的文本:14 个字符
    logMessage(true)
}

控制流

智能转换不仅适用于 if 条件表达式,也适用于 when 表达式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fun processInput(data: Any) {
    when (data) {
        // data 被自动转换为 Int
        is Int -> println("Log: Assigned new ID ${data + 1}")
        // data 被自动转换为 String
        is String -> println("Log: Received message \"$data\"")
        // data 被自动转换为 IntArray
        is IntArray -> println("Log: Processed scores, total = ${data.sum()}")
    }
}

fun main() {
    processInput(1001)
    // 日志:分配了新的 ID 1002
    processInput("System rebooted")
    // 日志:收到消息 "System rebooted"
    processInput(intArrayOf(10, 20, 30))
    // 日志:已处理分数,总计 = 60
}

也适用于 while 循环:

 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
26
27
28
29
30
31
32
33
34
35
sealed interface Status
data class Ok(val currentRoom: String) : Status
data object Error : Status

class RobotVacuum(val rooms: List<String>) {
    var index = 0

    fun status(): Status =
        if (index < rooms.size) Ok(rooms[index])
        else Error

    fun clean(): Status {
        println("Finished cleaning ${rooms[index]}")
        index++
        return status()
    }
}

fun main() {
    val robo = RobotVacuum(listOf("Living Room", "Kitchen", "Hallway"))

    var status: Status = robo.status()
    while (status is Ok) {
        // 编译器把 status 智能转换为 Ok 类型,因此可以访问 currentRoom
        // 属性。
        println("Cleaning ${status.currentRoom}...")
        status = robo.clean()
    }
    // 正在清扫客厅……
    // 客厅清扫完成
    // 正在清扫厨房……
    // 厨房清扫完成
    // 正在清扫走廊……
    // 走廊清扫完成
}

在这个例子中,密封接口 Status 有两个实现:数据类 Ok 和数据对象 Error。只有 Ok 数据类才有 currentRoom 属性。当 while 循环条件为真时,编译器会把 status 变量智能转换为 Ok 类型,从而可以在循环体中访问 currentRoom 属性。

如果你在使用 if、when 或 while 条件之前先声明一个 Boolean 类型的变量,那么编译器收集到的关于该变量的信息在对应的代码块中就可以用于智能转换。

当你希望把布尔条件提取为变量时,这会很有用。你可以给变量起一个有意义的名称,从而提高代码可读性,并能在后续代码中复用它。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Cat {
    fun purr() {
        println("Purr purr")
    }
}
fun petAnimal(animal: Any) {
    val isCat = animal is Cat
    if (isCat) {
        // 编译器可以获取关于
        // isCat 的信息,因此它知道 animal 已被智能转换
        // 为 Cat 类型。
        // 所以可以调用 purr() 函数。
        animal.purr()
    }
}

fun main(){
    val kitty = Cat()
    petAnimal(kitty)
    // 呼噜呼噜
}

逻辑运算符

如果左侧存在类型检查(普通检查或否定检查),编译器可以在 && 或 || 运算符的右侧执行智能转换:

1
2
3
4
5
6
7
// 在 `||` 右侧,x 会被自动转换为 String
if (x !is String || x.length == 0) return

// 在 `&&` 右侧,x 会被自动转换为 String
if (x is String && x.length > 0) {
    print(x.length) // x 会被自动转换为 String
}

如果你用 and 运算符(&&)组合多个类型检查,编译器会把对象同时智能转换为所有被检查的类型。更多内容请参阅交集类型。

如果你用 or 运算符(||)组合多个类型检查,则会智能转换为它们最近的公共超类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface Status {
    fun signal() {}
}

interface Ok : Status
interface Postponed : Status
interface Declined : Status

fun signalCheck(signalStatus: Any) {
    if (signalStatus is Postponed || signalStatus is Declined) {
        // signalStatus 被智能转换为公共超类型 Status
        signalStatus.signal()
    }
}

注意: 公共超类型只是联合类型的近似。Kotlin 目前不支持联合类型。

交集类型

当编译器通过多个 && 检查对对象进行智能转换时,它会推断出一个交集类型。这是一种内部类型,同时满足所有被检查的约束:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface Bird {
    fun fly()
}

interface Fish {
    fun swim()
}

fun describe(animal: Any) {
    // 推断出 Bird 和 Fish 类型
    if (animal is Bird && animal is Fish) {
        // 无需额外的检查或转换即可访问 fly() 和 swim()
        animal.fly()
        animal.swim()
    }
}

交集类型无法显式书写(non-denotable)。它们只存在于编译器的内部类型系统中,用于在类型检查期间保留类型信息,你无法在 Kotlin 代码里直接写出它们。你可能会在编译器错误信息和 IDE 提示中看到交集类型,通常显示为 A & B。唯一的例外是 T & Any,它用于声明确定非空类型。这种语法专门用于把类型参数与 Any 组合起来。

1
fun  <T> T.assertNotNull(): T & Any = this ?: throw IllegalStateException("null value")

内联函数

对于传递给内联函数的 lambda 函数所捕获的变量,编译器可以进行智能转换。

内联函数被视为带有隐式的 callsInPlace 契约。这意味着传给内联函数的任何 lambda 函数都会就地调用。由于 lambda 函数是就地调用的,编译器知道 lambda 函数不会泄露对其函数体内任何变量的引用。

编译器会结合这一信息以及其他分析,判断对捕获变量进行智能转换是否安全。例如:

 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
26
27
interface Processor {
    fun process()
}

inline fun inlineAction(f: () -> Unit) = f()

fun nextProcessor(): Processor? = null

fun runProcessor(): Processor? {
    var processor: Processor? = null
    inlineAction {
        // 编译器知道 processor 是局部变量,而 inlineAction()
        // 是内联函数,因此对 processor 的引用不会泄露。
        // 所以,对 processor 进行智能转换是安全的。

        // 如果 processor 不为 null,则 processor 会被智能转换
        if (processor != null) {
            // 编译器知道 processor 不为 null,因此不需要
            // 安全调用
            processor.process()
        }

        processor = nextProcessor()
    }

    return processor
}

异常处理

智能转换的信息会传递给 catch 和 finally 块。由于编译器会跟踪对象是否具有可空类型,这让你的代码更安全。例如:

 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
26
fun testString() {
    var stringInput: String? = null
    // stringInput 被智能转换为 String 类型
    stringInput = ""
    try {
        // 编译器知道 stringInput 不为 null
        println(stringInput.length)
        // 0

        // 编译器会丢弃此前关于 stringInput 的智能转换信息。
        // 现在 stringInput 的类型是 String?。
        stringInput = null

        // 触发一个异常
        if (2 > 1) throw Exception()
        stringInput = ""
    } catch (exception: Exception) {
        // 编译器知道 stringInput 可能为 null,
        // 因此 stringInput 保持可空。
        println(stringInput?.length)
        // null
    }
}
fun main() {
    testString()
}

智能转换的前提条件

只有当编译器能够保证变量在检查与使用之间不会发生变化时,智能转换才有效。它可以用于以下情况:

val 局部变量总是可以,局部委托属性除外。
val 属性如果该属性是 private、internal,或者检查是在声明该属性的同一模块中进行的。open 属性或具有自定义 getter 的属性不能使用智能转换。
var 局部变量如果变量在检查与使用之间没有被修改、没有被捕获进会修改它的 lambda 中,并且不是局部委托属性。
var 属性永远不行,因为变量随时可能被其他代码修改。

as 和 as? 转换运算符

Kotlin 有两个转换运算符:as 和 as?。两者都可以用于转换,但行为不同。

如果使用 as 运算符的转换失败,运行时会抛出 ClassCastException。这也是它被称为不安全运算符的原因。在转换为非空类型时可以使用 as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun main() {
    val rawInput: Any = "user-1234"

    // 成功转换为 String
    val userId = rawInput as String
    println("Logging in user with ID: $userId")
    // 正在登录 ID 为 user-1234 的用户

    // 触发 ClassCastException
    val wrongCast = rawInput as Int
    println("wrongCast contains: $wrongCast")
    // 线程 "main" 中出现异常 java.lang.ClassCastException
}

如果改用 as? 运算符,转换失败时该运算符会返回 null。这也是它被称为安全运算符的原因:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun main() {
    val rawInput: Any = "user-1234"

    // 成功转换为 String
    val userId = rawInput as? String
    println("Logging in user with ID: $userId")
    // 正在登录 ID 为 user-1234 的用户

    // 给 wrongCast 赋值为 null
    val wrongCast = rawInput as? Int
    println("wrongCast contains: $wrongCast")
    // wrongCast 的内容为:null
}

若要安全地转换可空类型,请使用 as? 运算符,以免转换失败时触发 ClassCastException。

你可以对可空类型使用 as。这样结果可以是 null,但如果转换不成功,它仍然会抛出 ClassCastException。因此,as? 是更安全的选择:

 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
26
27
fun main() {
    val config: Map<String, Any?> = mapOf(
        "username" to "kodee",
        "alias" to null,
        "loginAttempts" to 3
    )

    // 不安全地转换为可空 String
    val username: String? = config["username"] as String?
    println("Username: $username")
    // 用户名:kodee

    // 不安全地把 null 值转换为可空 String
    val alias: String? = config["alias"] as String?
    println("Alias: $alias")
    // 别名:null

    // 无法转换为可空 String,抛出 ClassCastException
    // val unsafeAttempts: String? = config["loginAttempts"] as String?
    // println("Login attempts (unsafe): $unsafeAttempts")
    // 线程 "main" 中出现异常 java.lang.ClassCastException

    // 无法转换为可空 String,返回 null
    val safeAttempts: String? = config["loginAttempts"] as? String
    println("Login attempts (safe): $safeAttempts")
    // 登录尝试次数(安全):null
}

向上转型与向下转型

在 Kotlin 中,你可以把对象转换为超类型和子类型。

把对象转换为其超类的实例称为向上转型(upcasting)。向上转型不需要任何特殊语法或转换运算符。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
interface Animal {
    fun makeSound()
}

class Dog : Animal {
    // 实现 makeSound() 的行为
    override fun makeSound() {
        println("Dog says woof!")
    }
}

fun printAnimalInfo(animal: Animal) {
    animal.makeSound()
}

fun main() {
    val dog = Dog()
    // 把 Dog 实例向上转型为 Animal
    printAnimalInfo(dog)
    // 狗汪汪叫!
}

在这个例子中,当用 Dog 实例调用 printAnimalInfo() 函数时,编译器会把它向上转型为 Animal,因为那是期望的参数类型。由于实际对象仍然是 Dog 实例,编译器会动态解析 Dog 类中的 makeSound() 函数,打印 "Dog says woof!"。

在行为依赖于抽象类型的 Kotlin API 中,你经常能看到显式的向上转型。在 Jetpack Compose 和 UI 工具包中也很常见,它们通常把所有 UI 元素当作超类型处理,之后再针对具体的子类进行操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    val textView = TextView(this)
    textView.text = "Hello, View!"

    // 从 TextView 向上转型为 View
    val view: View = textView

    // 使用 View 的函数
    view.setPadding(20, 20, 20, 20)
    // Activity 期望接收 View 类型
    setContentView(view)

把对象转换为其子类的实例称为向下转型(downcasting)。由于向下转型可能不安全,你需要使用显式的转换运算符。为避免转换失败时抛出异常,我们建议使用安全转换运算符 as?,在转换失败时返回 null:

 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
26
interface Animal {
    fun makeSound()
}

class Dog : Animal {
    override fun makeSound() {
        println("Dog says woof!")
    }

    fun bark() {
        println("BARK!")
    }
}

fun main() {
    // 创建 animal,它是类型为 Animal 的
    // Dog 实例
    val animal: Animal = Dog()

    // 安全地把 animal 向下转型为 Dog 类型
    val dog: Dog? = animal as? Dog

    // 使用安全调用,在 dog 不为 null 时调用 bark()
    dog?.bark()
    // "汪汪!"
}

在这个例子中,animal 声明的类型是 Animal,但实际保存的是 Dog 实例。代码安全地把 animal 转换为 Dog 类型,并使用安全调用(?.)访问 bark() 函数。

在序列化时会把基类反序列化为具体的子类型,这时会用到向下转型。在处理返回超类型对象的 Java 库时也很常见,你可能需要在 Kotlin 中把它们向下转型。