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

多态性

多态性 是一个古希腊术语,意为“多种形式”。在编程中,多态性意味着一个对象或其成员具有多个实现。

考虑一个简单的 Pet 类型的层次结构。Pet 类表示所有的宠物都可以 speak()(说话)。DogCat 覆盖了 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() 函数的参数。当将 DogCat 传递给 talk() 时,具体类型被遗忘,变成了一个普通的 Pet —— 既 DogCat 都被向上转型Pet。现在,这些对象被视为普通的 Pet,那么行 [1][2] 中的输出应该都是 "Pet",对吗?

talk() 不知道它接收到的 Pet 的确切类型。尽管如此,在通过基类 Pet 的引用调用 speak() 时,会调用正确的子类实现,并获得所需的行为。

多态性发生在父类引用包含子类实例时。当您在父类引用上调用成员时,多态性会从子类中产生正确的重写成员。

将函数调用与函数体连接在一起称为绑定。通常情况下,您不会对绑定多想,因为它在编译时静态地发生。在多态性中,相同的操作必须对不同的类型产生不同的行为 —— 但编译器无法提前知道要使用哪个函数体。函数体必须在运行时动态确定,使用动态绑定。动态绑定也称为晚期绑定动态调度。只有在运行时,Kotlin 才能确定要调用的确切 speak() 函数。因此,我们说多态性调用 pet.speak() 的绑定是动态发生的。

考虑一个幻想游戏。游戏中的每个 Character 都有一个 name,并且可以 play()。我们将 FighterMagician 结合起来来构建特定的角色:

// 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 时都会向上转型为 Charactertrace 显示了在 List 中的每个 Character 上调用 playTurn() 会产生不同的输出。

playTurn() 是基类 Character 上的扩展函数。当在行 [3] 中调用它时,它是静态绑定的,这意味着要调用的确切函数在编译时确定。在行 [3] 中,编译器确定只有一个 playTurn() 函数实现 —— 就是在行 [1] 上定义的那个。

当编译器分析行 [2] 中的 play() 函数调用时,它不知道要使用哪个函数实现。如果 Character 是一个 Elf,它必须调用 Elfplay()。如果 Character 是一个 FightingElf,它必须调用 FightingElfplay()。它还可能需要调用一个尚未定义的子类函数。函数绑定在每次调用时都会有所不同。在编译时,唯一确定的是行 [2] 中的 play()Character 子类的成员函数。具体的子类只能在运行时根据实际的 Character 类型来确定。

  • -

动态绑定是不是免费的。决定运行时类型的额外逻辑会对性能产生轻微的影响,与静态绑定相比。为了强制清晰,Kotlin 默认为封闭的类和成员函数。要继承和重写,必须明确指定。

when 语句这样的语言特性可以独立学习。多态性不可以 —— 它只在协调中工作,作为类关系的更大画面的一部分。为了有效地使用面向对象的技术,您必须扩展您的视角,不仅包括个体类的成员,还包括类之间的共性以及它们相互之间的关系。

***练

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