Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

向下转型

向下转型 发现先前向上转型的对象的特定类型。

向上转型总是安全的,因为基类不能具有比派生类更大的接口。每个基类成员都有保证存在,并且因此可以安全调用。尽管面向对象编程主要专注于向上转型,但在某些情况下,向下转型可能是一种有用且方便的方法。

向下转型发生在运行时,也称为运行时类型标识(RTTI)。

考虑一个类层次结构,其中基类型的接口比派生类型更窄。如果将对象向上转型为基类型,则编译器不再知道具体类型。特别是,它无法知道可以安全调用哪些扩展函数:

// DownCasting/NarrowingUpcast.kt
package downcasting

interface Base {
  fun f()
}

class Derived1 : Base {
  override fun f() {}
  fun g() {}
}

class Derived2 : Base {
  override fun f() {}
  fun h() {}
}

fun main() {
  val b1: Base = Derived1() // 向上转型
  b1.f()    // Base 的一部分
  // b1.g() // 不是 Base 的一部分
  val b2: Base = Derived2() // 向上转型
  b2.f()    // Base 的一部分
  // b2.h() // 不是 Base 的一部分
}

要解决这个问题,必须有一些方法来确保向下转型是正确的,这样您不会意外地将其转换为错误的类型并调用不存在的成员。

智能类型转换

在 Kotlin 中,智能类型转换是自动向下转型。关键字 is 检查对象是否为特定类型。在该检查的范围内,任何代码都会假定它该类型:

// DownCasting/IsKeyword.kt
import downcasting.*

fun main() {
  val b1: Base = Derived1() // 向上转型
  if(b1 is Derived1)
    b1.g() // 在 "is" 检查的范围内
  val b2: Base = Derived2() // 向上转型
  if(b2 is Derived2)
    b2.h() // 在 "is" 检查的范围内
}

如果 b1 的类型是 Derived1,则可以调用 g()。如果 b2 的类型是 Derived2,则可以调用 h()

智能类型转换在 when 表达式内尤其有用,该表达式使用 is 来搜索 when 参数的类型。注意,在 main() 中,每个特定类型首先被向上转型为 Creature,然后传递给 what()

// DownCasting/Creature.kt
package downcasting
import atomictest.eq

interface Creature

class Human : Creature {
  fun greeting() = "I'm Human"
}

class Dog : Creature {
  fun bark() = "Yip!"
}

class Alien : Creature {
  fun mobility() = "Three legs"
}

fun what(c: Creature): String =
  when (c) {
    is Human -> c.greeting()
    is Dog -> c.bark()
    is Alien -> c.mobility()
    else -> "Something else"
  }

fun main() {
  val c: Creature = Human()
  what(c) eq "I'm Human"
  what(Dog()) eq "Yip!"
  what(Alien()) eq "Three legs"
  class Who : Creature
  what(Who()) eq "Something else"
}

main() 中,向上转型发生在将 Human 赋给 Creature、将 Dog 传递给 what()、将 Alien 传递给 what() 以及将 Who 传递给 what()

类层次结构通常在基类在顶部,派生类在其下方的方式下进行绘制。what() 接受先前向上转型的 Creature 并发现其确切类型,因此将该 Creature 对象向下转换到继承层次结构中,从更通用的基类到更特定的派生类。

when 表达式生成一个值时,需要一个 else 分支来捕获所有剩余的可能性。在 main() 中,使用局部类 Who 的实例来测试 else 分支。

when 的每个分支都使用 c,好像它是我们检查的类型:如果 cHuman,则调用 greeting();如果它是 Dog,则调用 bark();如果它是 Alien,则调用 mobility()

可修改的引用

自动向下转型受到特殊约束。如果对对象的基类引用是可修改的(一个 var),那么在检测类型和在调用向下转型对象上的特定函数之间,有可能将此引用分配给不同的对象。也就是说,在类型检测和使用之间,对象的具体类型可能会发生变化。

在以下示例中,cwhen 的参数,Kotlin 坚持要求该参数是不可变的,以便在 is 表达式和 -> 后面的调用之间不能更改它:

// DownCasting/MutableSmartCast.kt
package downcasting

class SmartCast1(val c: Creature) {
  fun contact() {
    when (c) {
      is Human -> c.greeting()
      is Dog -> c.bark()
      is Alien -> c.mobility()
    }
  }
}

class SmartCast2(var c: Creature) {
  fun contact() {
    when (val c = c) {           // [1]
      is Human -> c.greeting()   // [2]
      is Dog -> c.bark()
      is Alien -> c.mobility()
    }
  }
}

SmartCast1 中,构造函数参数 `c

是一个val,在 SmartCast2中是一个var。在两种情况下,c都传递到when` 表达式,该表达式使用一系列智能类型转换。

[1] 中,表达式 val c = c 看起来有些奇怪,只是在这里出于方便才使用——我们不建议在正常代码中“遮蔽”标识符名称。val c 创建一个新的局部标识符 c,用于捕获属性 c 的值。然而,属性 c 是一个 var,而局部(遮蔽)c 是一个 val。尝试移除 val c =。这意味着现在 c 将成为属性,即一个 var。这将导致 [2] 处产生错误消息:

  • 智能转换到 'Human' 是不可能的,因为 'c' 是一个可变属性,可能在此时被更改

is Dogis Alien 会产生类似的消息。这不仅限于 while 表达式;还有其他情况也会产生相同的错误消息。

错误消息中描述的更改通常是通过并发发生的,当多个独立任务有机会在不可预测的时间更改 c 时。(并发是一个高级主题,我们在本书中不涵盖该主题)。

Kotlin 强制我们确保在执行类型检查和使用向下转型类型之间不会更改 cSmartCast1 通过使属性 c 成为 val 来实现这一点,而 SmartCast2 则通过引入局部的 val c 来实现。

同样地,复杂表达式不能智能转换,因为表达式可能会重新评估。对于可继承的 open 属性,不能进行智能转换,因为其值可能会在子类中被覆盖,因此不能保证在下一次访问时值将保持相同。

as 关键字

as 关键字将一个通用类型强制转换为特定类型:

// DownCasting/Unsafe.kt
package downcasting
import atomictest.*

fun dogBarkUnsafe(c: Creature) =
  (c as Dog).bark()

fun dogBarkUnsafe2(c: Creature): String {
  c as Dog
  c.bark()
  return c.bark() + c.bark()
}

fun main() {
  dogBarkUnsafe(Dog()) eq "Yip!"
  dogBarkUnsafe2(Dog()) eq "Yip!Yip!"
  (capture {
    dogBarkUnsafe(Human())
  }) contains listOf("ClassCastException")
}

dogBarkUnsafe2() 显示了 as 的第二种形式:如果说 c as Dog,那么在作用域的其余部分中,c 将被视为 Dog

失败的 as 转换会抛出 ClassCastException。普通的 as 被称为不安全的转换

安全转换 as? 失败时,它不会抛出异常,而是返回 null。然后,您必须合理地处理该 null,以防止后续出现 NullPointerException。Elvis 运算符(在 安全调用与 Elvis 运算符 中描述)通常是最直接的方法:

// DownCasting/Safe.kt
package downcasting
import atomictest.eq

fun dogBarkSafe(c: Creature) =
  (c as? Dog)?.bark() ?: "Not a Dog"

fun main() {
  dogBarkSafe(Dog()) eq "Yip!"
  dogBarkSafe(Human()) eq "Not a Dog"
}

如果 c 不是 Dog,则 as? 会产生一个 null。因此,(c as? Dog) 是一个可空表达式,我们必须使用安全调用运算符 ?. 来调用 bark()。如果 as? 产生一个 null,那么整个表达式 (c as? Dog)?.bark() 也将产生一个 null,Elvis 运算符通过生成 "Not a Dog" 来处理这个 null

在列表中发现类型

当在谓词中使用时,is 可以在 List 或任何可迭代对象(可以迭代的对象)中查找特定类型的对象:

// DownCasting/FindType.kt
package downcasting
import atomictest.eq

val group: List<Creature> = listOf(
  Human(), Human(), Dog(), Alien(), Dog()
)

fun main() {
  val dog = group
    .find { it is Dog } as Dog?    // [1]
  dog?.bark() eq "Yip!"            // [2]
}

因为 group 包含 Creature,所以 find() 返回一个 Creature。我们希望将其视为 Dog,因此我们在第 [1] 行显式地将其转换。在 group 中可能没有 Dog,在这种情况下,find() 返回一个 null,因此我们必须将结果转换为可空的 Dog?。因为 dog 是可空的,所以我们在第 [2] 行使用安全调用运算符。

通常可以通过使用 filterIsInstance() 来避免第 [1] 行中的代码,它可以生成特定类型的所有元素:

// DownCasting/FilterIsInstance.kt
import downcasting.*
import atomictest.eq

fun main() {
  val humans1: List<Creature> =


    group.filter { it is Human }
  humans1.size eq 2
  val humans2: List<Human> =
    group.filterIsInstance<Human>()
  humans2 eq humans1
}

filterIsInstance() 是产生与 filter() 相同结果的更可读的方式。然而,结果类型是不同的:虽然 filter() 返回 Creature 的列表(即使所有的结果元素都是 Human),但 filterIsInstance() 返回目标类型 Human 的列表。我们还消除了 FindType.kt 中出现的空值问题。

练习和解答可以在 www.AtomicKotlin.com 上找到。