多态性
多态性 是一个古希腊术语,意为“多种形式”。在编程中,多态性意味着一个对象或其成员具有多个实现。
考虑一个简单的 Pet
类型的层次结构。Pet
类表示所有的宠物都可以 speak()
(说话)。Dog
和 Cat
覆盖了 speak()
成员函数:
// Polymorphism/Pet.kt
package polymorphism
import atomictest.eq
open class Pet {
open fun speak() = "Pet"
}
class Dog : Pet() {
override fun speak() = "Bark!"
}
class Cat : Pet() {
override fun speak() = "Meow"
}
fun talk(pet: Pet) = pet.speak()
fun main() {
talk(Dog()) eq "Bark!" // [1]
talk(Cat()) eq "Meow" // [2]
}
注意 talk()
函数的参数。当将 Dog
或 Cat
传递给 talk()
时,具体类型被遗忘,变成了一个普通的 Pet
—— 既 Dog
和 Cat
都被向上转型为 Pet
。现在,这些对象被视为普通的 Pet
,那么行 [1] 和 [2] 中的输出应该都是 "Pet"
,对吗?
talk()
不知道它接收到的 Pet
的确切类型。尽管如此,在通过基类 Pet
的引用调用 speak()
时,会调用正确的子类实现,并获得所需的行为。
多态性发生在父类引用包含子类实例时。当您在父类引用上调用成员时,多态性会从子类中产生正确的重写成员。
将函数调用与函数体连接在一起称为绑定。通常情况下,您不会对绑定多想,因为它在编译时静态地发生。在多态性中,相同的操作必须对不同的类型产生不同的行为 —— 但编译器无法提前知道要使用哪个函数体。函数体必须在运行时动态确定,使用动态绑定。动态绑定也称为晚期绑定或动态调度。只有在运行时,Kotlin 才能确定要调用的确切 speak()
函数。因此,我们说多态性调用 pet.speak()
的绑定是动态发生的。
考虑一个幻想游戏。游戏中的每个 Character
都有一个 name
,并且可以 play()
。我们将 Fighter
和 Magician
结合起来来构建特定的角色:
// Polymorphism/FantasyGame.kt
package polymorphism
import atomictest.*
abstract class Character(val name: String) {
abstract fun play(): String
}
interface Fighter {
fun fight() = "Fight!"
}
interface Magician {
fun doMagic() = "Magic!"
}
class Warrior :
Character("Warrior"), Fighter {
override fun play() = fight()
}
open class Elf(name: String = "Elf") :
Character(name), Magician {
override fun play() = doMagic()
}
class FightingElf :
Elf("FightingElf"), Fighter {
override fun play() =
super.play() + fight()
}
fun Character.playTurn() = // [1]
trace(name + ": " + play()) // [2]
fun main() {
val characters: List<Character> = listOf(
Warrior(), Elf(), FightingElf()
)
characters.forEach { it.playTurn() } // [3]
trace eq """
Warrior: Fight!
Elf: Magic!
FightingElf: Magic!Fight!
"""
}
在 main()
中,每个对象在放入 List
时都会向上转型为 Character
。trace
显示了在 List
中的每个 Character
上调用 playTurn()
会产生不同的输出。
playTurn()
是基类 Character
上的扩展函数。当在行 [3] 中调用它时,它是静态绑定的,这意味着要调用的确切函数在编译时确定。在行 [3] 中,编译器确定只有一个 playTurn()
函数实现 —— 就是在行 [1] 上定义的那个。
当编译器分析行 [2] 中的 play()
函数调用时,它不知道要使用哪个函数实现。如果 Character
是一个 Elf
,它必须调用 Elf
的 play()
。如果 Character
是一个 FightingElf
,它必须调用 FightingElf
的 play()
。它还可能需要调用一个尚未定义的子类函数。函数绑定在每次调用时都会有所不同。在编译时,唯一确定的是行 [2] 中的 play()
是 Character
子类的成员函数。具体的子类只能在运行时根据实际的 Character
类型来确定。
- -
动态绑定是不是免费的。决定运行时类型的额外逻辑会对性能产生轻微的影响,与静态绑定相比。为了强制清晰,Kotlin 默认为封闭的类和成员函数。要继承和重写,必须明确指定。
像 when
语句这样的语言特性可以独立学习。多态性不可以 —— 它只在协调中工作,作为类关系的更大画面的一部分。为了有效地使用面向对象的技术,您必须扩展您的视角,不仅包括个体类的成员,还包括类之间的共性以及它们相互之间的关系。
***练
习和解答可以在 www.AtomicKotlin.com 找到。***