动态分派
本课属于 Coddy Kotlin 学习旅程的“面向对象编程”单元。第 27 课,共 57 课。
多态表示一次调用可以有多种表现方式。通过超类或接口类型调用被重写的方法时,Kotlin 会在程序运行时查看对象的实际类,并执行该类的版本。这就是动态分派:
abstract class Animal(val name: String) {
abstract fun sound(): String
fun speak() = "$name: ${sound()}"
}
class Dog(name: String) : Animal(name) {
override fun sound() = "Woof"
}
class Cow(name: String) : Animal(name) {
override fun sound() = "Moo"
}在 main 中:
val barn: List<Animal> = listOf(Dog("Rex"), Cow("Bess"), Dog("Fido"))
for (a in barn) println(a.speak())输出:
Rex: Woof
Bess: Moo
Fido: Woof当出现新的子类时,调用代码不会改变。下面的 totalArea 是为任意 Shape 编写的,因此它也适用于 Triangle,尽管没有人修改它:
interface Shape {
fun area(): Double
}
class Square(val side: Double) : Shape {
override fun area() = side * side
}
class Triangle(val base: Double, val height: Double) : Shape {
override fun area() = base * height / 2
}
fun totalArea(shapes: List<Shape>) = shapes.sumOf { it.area() }在 main 中:
println(totalArea(listOf(Square(2.0), Triangle(3.0, 4.0))))输出:
10.0分派查看的是对象,而不是变量。将另一个对象赋给同一个变量,会改变下次调用时运行的代码:
interface Mode {
fun format(text: String): String
}
class Normal : Mode {
override fun format(text: String) = text
}
class Shout : Mode {
override fun format(text: String) = text.uppercase() + "!"
}在 main 中:
var mode: Mode = Normal()
println(mode.format("hello"))
mode = Shout()
println(mode.format("hello"))输出:
hello
HELLO!多态可以替代 if (kind == "dog") 这样的检查,否则每当使用动物时都要重复进行这些检查。每个类都保留自己的行为,添加一种动物就意味着添加一个类:
// 没有多态:每个函数都要检查类型
fun sound(kind: String) = when (kind) { "dog" -> "Woof"; "cow" -> "Moo"; else -> "?" }
// 有了多态:新类型自带自己的行为
class Sheep(name: String) : Animal(name) {
override fun sound() = "Baa"
}挑战
简单Character(name, hp) 是一个开放类。takeHit(damage) 会从 hp 中减去伤害(最低不会低于 0),而 status() 会返回 Ada: 30 hp,或者在生命值为 0 时返回 Ada: down。你可以在任何地方读取 hp,但只能在 Character 内修改它。通过重写 takeHit 并将调整后的伤害传给 super.takeHit,完成这些子类:Warrior(40 点生命值)受到的伤害减半,向下取整;Mage(25 点生命值)有一个护盾,可以吸收第一次受到的全部攻击;Rogue(30 点生命值)会躲避它受到的每第二次攻击(第 2 次、第 4 次,依此类推)。
提供的代码会按 warrior:Ada,mage:Bo,rogue:Cy 读取队伍,然后执行 all 10 或 Ada 12 攻击。它会通过 Character 类型调用 takeHit,最后打印每个角色的 status()。
你的代码应写在 Character.kt、Warrior.kt、Mage.kt 和 Rogue.kt 中。Main.kt 包含提供的输入/输出代码,不能编辑。
亲手试试
fun main() {
// 提供的输入输出代码:保持原样。
val input = generateSequence(::readLine).toList()
val party = input[0].split(",").map { spec ->
val (kind, name) = spec.split(":")
when (kind) {
"warrior" -> Warrior(name)
"mage" -> Mage(name)
else -> Rogue(name)
}
}
for (cmd in input.drop(1)) {
val (target, damage) = cmd.split(" ")
for (c in party) {
if (target == "all" || target == c.name) c.takeHit(damage.toInt())
}
}
for (c in party) println(c.status())
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Kotlin 在线编辑器