Menu
CoddyTech

密封类层次结构

本课属于 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)

因为集合是封闭的,对密封类型使用 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 分支不需要 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

枚举还是密封层次结构?枚举是一组固定的单例,它们都具有相同的属性。密封层次结构允许每种情况携带自己的数据,而且像 Move 这样的情况可以有多个具有不同值的实例:

enum class Light { RED, YELLOW, GREEN }          // 结构相同,每种各有一个实例

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

挑战

简单

机器人理解密封接口 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 在线编辑器