Menu
Coddy logo textTech

シールド階層

CoddyのKotlinジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 34/57。

sealed クラスまたはインターフェースには、直接サブクラスの閉じた集合があり、それらはすべて同じパッケージとモジュール内で宣言されます。コンパイラーはそのすべてを把握しているため、形の数が固定された結果に適しています。

sealed interface Result
data class Success(val value: Int) : Result
data class Failure(val reason: String) : Result

fun parseAge(text: String): Result {
    val n = text.toIntOrNull() ?: return Failure("not a number: $text")
    return if (n in 0..150) Success(n) else Failure("out of range: $n")
}

main の中で:

println(parseAge("42"))
println(parseAge("abc"))
println(parseAge("200"))

出力:

Success(value=42)
Failure(reason=not a number: abc)
Failure(reason=out of range: 200)

集合が閉じているため、sealed 型に対する when は else なしでも網羅的です。後からサブクラスが追加されると、そのような when は新しいケースを処理するまでコンパイルできなくなるため、ケースを忘れることがありません。

sealed interface Result
data class Success(val value: Int) : Result
data class Failure(val reason: String) : Result

fun describe(r: Result) = when (r) {
    is Success -> "ok: ${r.value}"
    is Failure -> "error: ${r.reason}"
}

main の内部では:

println(describe(Success(7)))
println(describe(Failure("timeout")))

出力:

ok: 7
error: timeout

データを持たないケースはdata objectです。これは、読み取り可能なtoString()を持つ単一のインスタンスです。そのwhenブランチでは、インスタンスが1つしかないためisは必要ありません。その他のケースには、データクラス、オブジェクト、または通常のクラスを使用できます。

sealed interface Command
data object Quit : Command
data class Move(val steps: Int) : Command
data class Say(val text: String) : Command

fun parse(line: String): Command = when {
    line == "quit" -> Quit
    line.startsWith("move ") -> Move(line.drop(5).toInt())
    else -> Say(line)
}

fun run(c: Command) = when (c) {
    Quit -> "bye"
    is Move -> "moving ${c.steps}"
    is Say -> "saying ${c.text}"
}

mainの内部では、次のようにします。

for (line in listOf("move 3", "hello", "quit")) {
    val c = parse(line)
    println("$c: ${run(c)}")
}

出力:

Move(steps=3): moving 3
Say(text=hello): saying hello
Quit: bye

Enum と sealed 階層のどちらを使うべきでしょうか?Enum は、すべて同じプロパティを持つ単一インスタンスの固定された集合です。sealed 階層では、それぞれのケースが独自のデータを保持できます。また、Move のようなケースは、異なる値を持つ複数のインスタンスを作成できます。

enum class Light { RED, YELLOW, GREEN }          // 同じ形状で、それぞれ1インスタンス

sealed interface Payment                          // 各ケースが独自のデータを持つ
data class Card(val number: String) : Payment
data class Transfer(val iban: String, val reference: String) : Payment
data object Cash : Payment
challenge icon

チャレンジ

簡単

ロボットは、提供されている sealed interface Command のコマンド、つまり Move(steps)、Turn(left)、Report、Invalid(text) を理解します。parse(line) を完成させてください。move 3 は Move(3) を生成し、turn left と turn right は Turn を生成し、report は Report を生成します。有効な数値を伴わない移動を含め、それ以外はすべて、その行を使った Invalid を生成します。次に、網羅的な when を使用し、else を使わずに Robot.execute(command) を完成させてください。ロボットは (0, 0) から開始し、N の方向を向いています(北は y + 1、東は x + 1)。ロボットは moved to (0, 3)、facing W、at (0, 3) facing W、または ignored を返します。

提供されているコードは各行を解析し、Move(steps=3) -> moved to (0, 3) を出力します。

コードは Robot.kt、Parser.kt、Command.kt、Move.kt、Turn.kt、Report.kt、Invalid.kt に記述します。Main.kt には提供されている入力/出力コードが含まれており、編集できません。

自分で試してみよう

fun main() {
    // 提供された入出力コード: そのままにしておく
    val input = generateSequence(::readLine).toList()
    val robot = Robot()
    for (line in input) {
        val command = parse(line)
        println("$command -> ${robot.execute(command)}")
    }
}
quiz icon腕試し

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

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

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