向下转型
向下转型 发现先前向上转型的对象的特定类型。
向上转型总是安全的,因为基类不能具有比派生类更大的接口。每个基类成员都有保证存在,并且因此可以安全调用。尽管面向对象编程主要专注于向上转型,但在某些情况下,向下转型可能是一种有用且方便的方法。
向下转型发生在运行时,也称为运行时类型标识(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
,好像它是我们检查的类型:如果 c
是 Human
,则调用 greeting()
;如果它是 Dog
,则调用 bark()
;如果它是 Alien
,则调用 mobility()
。
可修改的引用
自动向下转型受到特殊约束。如果对对象的基类引用是可修改的(一个 var
),那么在检测类型和在调用向下转型对象上的特定函数之间,有可能将此引用分配给不同的对象。也就是说,在类型检测和使用之间,对象的具体类型可能会发生变化。
在以下示例中,c
是 when
的参数,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 Dog
和 is Alien
会产生类似的消息。这不仅限于 while
表达式;还有其他情况也会产生相同的错误消息。
错误消息中描述的更改通常是通过并发发生的,当多个独立任务有机会在不可预测的时间更改 c
时。(并发是一个高级主题,我们在本书中不涵盖该主题)。
Kotlin 强制我们确保在执行类型检查和使用向下转型类型之间不会更改 c
。SmartCast1
通过使属性 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 上找到。