Kotlin 密封类与密封接口
密封类用来表示一个受限的类层次:某个值的所有可能类型在编译期就完全确定。
正因为子类是已知的有限集合,when 表达式才能在没有 else 的情况下通过穷尽性检查。
它常用于状态机、接口返回结果、事件类型这类「只有这几种情况」的建模。
密封类的定义
用 sealed 修饰的类就是密封类,它本身是抽象的,不能直接实例化。
密封类的构造器默认是 protected,只有它的子类能够继承。
实例
sealed class RunoobResult
// 子类与密封类写在同一个文件中
data class Success(val data: String) : RunoobResult()
data class Failure(val message: String) : RunoobResult()
object Loading : RunoobResult()
fun main() {
val result: RunoobResult = Success("www.runoob.com")
println(result)
// RunoobResult() // 编译错误:密封类不能直接实例化
}
Success(data=www.runoob.com)
子类可以是普通类、数据类或 object,形态没有限制。
这也意味着每个子类可以携带完全不同的数据,这是密封类比枚举更灵活的地方。
子类的位置
密封类最容易被记错的规则,就是子类必须放在哪里。
Kotlin 1.0 要求所有子类必须嵌套写在密封类内部,这个限制早已取消。
Kotlin 1.1 起(KEEP-29)子类只需与密封类在同一个文件中即可,不必内嵌。
Kotlin 1.5 起进一步放宽到同一模块、同一包下的任意文件。
实例
// 推荐写法:子类与密封类在同一个文件,平级声明,不必嵌套
sealed class RunoobResult
class Success(val data: String) : RunoobResult()
class Failure(val message: String) : RunoobResult()
// 下面这种「必须内嵌」的写法是 Kotlin 1.0 的旧限制,现在不再需要
sealed class OldStyleResult {
class Success(val data: String) : OldStyleResult()
class Failure(val message: String) : OldStyleResult()
}
fun main() {
val a: RunoobResult = Success("runoob")
val b: OldStyleResult = OldStyleResult.Success("RUNOOB")
println(a.javaClass.simpleName)
println(b.javaClass.simpleName)
}
Success Success
注意:不要把「子类必须内嵌在密封类中」当成现在的规则。Kotlin 1.1 起只需同一文件,Kotlin 1.5 起同模块同包内的任意文件都可以。子类嵌套写仍然合法,但那是风格选择,不是语法要求。
密封接口(Kotlin 1.4 起)
Kotlin 1.4 起(KT-44016),接口也可以用 sealed 修饰。
密封接口与密封类的规则基本一致:直接子类必须在同一个文件中声明。
实例
// Kotlin 1.4 起,接口也可以声明为 sealed
sealed interface RunoobEvent
data class Click(val x: Int, val y: Int) : RunoobEvent
data class Scroll(val delta: Int) : RunoobEvent
data object Refresh : RunoobEvent // data object 需要 Kotlin 1.9 起
fun handle(event: RunoobEvent): String = when (event) {
is Click -> "点击坐标:(${event.x}, ${event.y})"
is Scroll -> "滚动:${event.delta}"
Refresh -> "刷新"
}
fun main() {
println(handle(Click(10, 20)))
println(handle(Scroll(-5)))
println(handle(Refresh))
}
点击坐标:(10, 20) 滚动:-5 刷新
密封接口比密封类多一个优势:一个类可以实现多个密封接口。
实例
sealed interface RunoobHoverable
// 一个类可以同时实现多个密封接口
class RunoobButton : RunoobClickable, RunoobHoverable
fun main() {
val button = RunoobButton()
println(button is RunoobClickable)
println(button is RunoobHoverable)
}
true true
如果只是需要一个封闭的类层次,不涉及状态继承,优先用密封接口。
与 when 的穷尽性配合
密封类最有价值的用法,就是让 when 分支的遗漏在编译期就暴露出来。
当 when 作为表达式使用时,只要覆盖了全部子类,就不需要写 else。
实例
object Idle : RunoobState()
object Running : RunoobState()
data class Error(val code: Int) : RunoobState()
}
// 作为表达式:必须覆盖所有分支,漏掉任何一个都编译不通过
fun label(state: RunoobState): String = when (state) {
RunoobState.Idle -> "空闲"
RunoobState.Running -> "运行中"
is RunoobState.Error -> "错误码 ${state.code}"
}
fun main() {
println(label(RunoobState.Idle))
println(label(RunoobState.Running))
println(label(RunoobState.Error(404)))
}
空闲 运行中 错误码 404
将来给密封类新增一个子类时,所有没处理它的 when 都会立刻报错,编译器帮你找出全部需要修改的地方。
Kotlin 1.6 起,对密封类型主语的 when 语句如果分支不穷尽会给出警告;Kotlin 1.7 起这变成了错误。
也就是说,无论 when 是表达式还是语句,都不该漏掉分支。
密封类与枚举的对比
两者都能配合穷尽性检查,但适用场景有明显区别。
| 对比项 | sealed class / sealed interface | enum class |
|---|---|---|
| 实例数量 | 每个子类可以有任意多个实例 | 每个常量在全局只有一个实例 |
| 携带数据 | 不同子类可以携带完全不同的属性 | 所有常量共享同一组属性 |
| 子类形态 | class、data class、object 都可以 | 只能是枚举常量本身 |
| 可变状态 | 可以有,每个实例互不影响 | 常量是单例,不适合存可变状态 |
| when 穷尽性 | 支持 | 支持 |
| 典型场景 | 状态机、请求结果、事件类型 | 星期、颜色、固定选项 |
一句话概括:枚举表达「固定的几个值」,密封类表达「固定的几种情况」。
实例
enum class RunoobStatus(val code: Int) {
OK(200), NOT_FOUND(404), TIMEOUT(0)
}
// 密封类:每个子类可以带自己的数据
sealed class RunoobResponse {
data class Ok(val body: String) : RunoobResponse()
data class NotFound(val path: String) : RunoobResponse()
data object Timeout : RunoobResponse()
}
fun describe(response: RunoobResponse): String = when (response) {
is RunoobResponse.Ok -> "返回内容:${response.body}"
is RunoobResponse.NotFound -> "找不到路径:${response.path}"
RunoobResponse.Timeout -> "请求超时"
}
fun main() {
println(RunoobStatus.OK.code)
println(describe(RunoobResponse.Ok("RUNOOB")))
println(describe(RunoobResponse.NotFound("/runoob")))
println(describe(RunoobResponse.Timeout))
}
200 返回内容:RUNOOB 找不到路径:/runoob 请求超时
常见问题
下面整理几个使用密封类时容易遇到的问题。
密封类能直接被实例化吗
不能,密封类本身是抽象的,必须通过它的子类创建对象。
object 形式的子类可以直接当实例使用。
密封类的子类还能再有子类吗
可以,密封类继承密封类也是允许的,直接子类仍然要满足位置要求。
间接子类只要自己的直接父类符合规则,就可以放在其他文件里。
什么时候该用密封接口而不是密封类
不涉及共享状态和继承实现,只是要一个封闭的标签集合时,用密封接口更轻。
需要给子类提供公共实现或公共属性时,用密封类更合适。
为什么 when 里不需要 else
因为密封类的子类集合在编译期是已知的,编译器能判断分支是否已经覆盖全部情况。
如果手动加了 else,反而会掩盖将来新增子类时的遗漏。
