密封类基础:什么是密封类、定义语法与枚举类的区别
好,咱们直接进入正题。
密封类(Sealed Class)这个概念,我第一次接触时觉得它挺「拧巴」的——它既像枚举,又像抽象类。后来在项目里踩过几次坑,才真正理解它的设计意图。说白了,密封类就是 Kotlin 给「有限子类型」问题准备的一把手术刀。
什么是密封类
先问个问题:你有没有遇到过这种情况——一个父类,你知道它只有固定的几种子类型,但编译器不知道?
比如网络请求的结果,要么是成功,要么是失败,要么是加载中。这三种情况,你心里门儿清。但如果你用普通抽象类,别人随便写个子类继承过去,你的 when 表达式就得加 else 分支。这就是痛点。
密封类就是来解决这个的。它告诉编译器:「嘿,我这个类的子类型就这么多,别的不许有。」
核心定义:密封类是一种受限的类层次结构。它的所有直接子类必须在同一个文件内声明,并且是已知的、有限的。
我个人习惯把密封类理解为「带状态的枚举」。枚举是固定实例,密封类是固定类型。你想想看,枚举的每个值都是同一个类的实例,而密封类的每个子类可以有自己的状态和行为。这个区别很关键。
密封类的定义与语法
语法其实很简单,用 sealed 关键字修饰类即可。来看个例子:
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String, val code: Int) : Result()
object Loading : Result()
}
嗯,这里要注意几个细节:
- 密封类本身是抽象的,不能直接实例化
- 子类可以是 data class、object 或者普通 class
- 所有子类必须在同一个文件里(Kotlin 1.1 之前要求更严格,现在放宽了,但建议还是放一起)
我在项目中遇到过这样一个场景:需要表示用户界面的不同状态。用密封类简直不要太舒服:
sealed class UiState {
data class Display(val items: List<Item>) : UiState()
data class Error(val message: String) : UiState()
object Loading : UiState()
object Empty : UiState()
}
配合 when 表达式使用时,编译器会帮你检查是否覆盖了所有分支:
fun render(state: UiState) {
when (state) {
is UiState.Display -> showList(state.items)
is UiState.Error -> showError(state.message)
UiState.Loading -> showSpinner()
UiState.Empty -> showEmptyHint()
}
// 不需要 else 分支!
}
我曾经因为漏掉一个分支导致线上 bug,自那以后,凡是有限状态集我都用密封类。编译器帮我兜底,心里踏实。
密封类与枚举类的区别
很多初学者会问:「枚举也能表示状态啊,为什么还要用密封类?」
好问题。咱们来对比一下:
| 对比维度 | 枚举类 | 密封类 |
|---|---|---|
| 本质 | 一组固定的常量实例 | 一组固定的子类型 |
| 每个值/子类的状态 | 所有实例结构相同 | 每个子类可以有不同的属性 |
| 实例数量 | 每个枚举值只有一个实例 | 每个子类可以有多个实例 |
| 数据携带 | 通过构造函数参数,但所有值参数相同 | 每个子类可以定义不同的数据字段 |
| 适用场景 | 简单的状态标识(如颜色、方向) | 需要携带不同数据的复杂状态 |
说白了,枚举适合「是什么」的问题,密封类适合「有什么」的问题。
举个例子:
// 枚举:只能表示状态名称
enum class Status { SUCCESS, ERROR, LOADING }
// 密封类:每个状态可以携带不同数据
sealed class Status {
data class Success(val data: String) : Status()
data class Error(val message: String, val code: Int) : Status()
object Loading : Status()
}
你看,枚举的 SUCCESS 就是一个光杆司令,而密封类的 Success 可以带着 data 到处跑。这就是本质区别。
我的建议:如果只是标记状态,不需要额外数据,用枚举就够了。一旦需要每个状态携带不同的数据,或者状态之间有行为差异,果断上密封类。
避坑指南
最后分享几个我踩过的坑:
我曾经在密封类里用了内部类,结果导致子类无法在外部访问。记住:密封类的子类必须是顶级类,或者嵌套在密封类内部,但不能是内部类(没有 inner 关键字)。
还有一个容易忽略的点:密封类的子类不能是局部类(定义在函数内部的类)。这个限制其实很合理——局部类的作用域太窄,违背了密封类「有限且已知」的设计初衷。
嗯,关于密封类的基础知识就聊到这里。记住三个关键词:有限子类型、编译器检查、状态携带。下一节咱们会深入密封类的实际应用场景,看看它在状态管理设计模式中到底能发挥多大威力。