Menu
Coddy logo textTech

動的ディスパッチ

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"
}
challenge icon

チャレンジ

簡単

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())
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Kotlinプレイグラウンド