動的ディスパッチ
CoddyのKotlinジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 27/57。
ポリモーフィズムとは、1つの呼び出しが多くの方法で動作できることを意味します。オーバーライドされたメソッドをスーパークラス型またはインターフェース型を通して呼び出すと、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") のようなチェックを置き換えます。各クラスが独自の振る舞いを保持し、動物の種類を追加するにはクラスを1つ追加します:
// ポリモーフィズムなし: すべての関数が種類をチェックする
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() は hp が 0 のとき Ada: down を、それ以外では Ada: 30 hp を返します。hp はどこからでも読み取れますが、変更できるのは Character の内部だけです。takeHit をオーバーライドし、変更後のダメージを super.takeHit に渡してサブクラスを完成させてください。Warrior(40 hp)はダメージを半分(小数点以下切り捨て)受け、Mage(25 hp)は最初の攻撃をすべて吸収するシールドを持ち、Rogue(30 hp)は受けた攻撃の 2 回目ごと(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プレイグラウンド