密封类基础回顾:sealed class 定义、枚举与密封类区别、密封接口、编译器穷举检查
密封类是 Kotlin 提供的受限类层次结构机制。与枚举不同,密封类的每个子类可以拥有完全不同的属性集和方法,这使其成为在编译器级别描述复杂状态和事件的理想选择。本章我们将系统回顾密封类的核心概念,为后续的事件总线实战打下坚实基础。
一、sealed class 定义与基本语法
密封类使用 sealed 修饰符声明,子类必须定义在同一个编译单元内(Kotlin 1.5+ 放宽了同一文件的限制,但仍需在同一模块中)。
// 定义一个密封类
sealed class UiState {
data class Loading(val progress: Int) : UiState()
data class Success(val data: List<String>) : UiState()
data class Error(val message: String, val code: Int) : UiState()
object Empty : UiState()
}
// 外部扩展密封类(同一编译单元)
data class Timeout(val detail: String) : UiState()
Kotlin 1.9 + 增强: 新版本编译器对密封类的子类搜索范围做了优化,即使是跨文件定义(同一 Gradle 模块),穷举检查依然生效。
二、枚举 vs 密封类:核心差异
枚举和密封类都可以表达“有限类型”,但语义和能力完全不同。枚举是常量实例的集合,密封类是子类多态的集合。以下从 5 个维度进行对比:
| 对比维度 | 枚举 (enum class) | 密封类 (sealed class) |
|---|---|---|
| 实例化 | 每个枚举常量是单例(object) | 子类可以有多个实例(data class / object) |
| 数据携带 | 所有常量共享相同的属性签名 | 每个子类可以携带不同的字段类型 |
| 层次结构 | 单层,一组命名的值 | 多层,支持继承和多态 |
| 何时使用 | 简单的状态机(ON/OFF/PENDING) | 复杂状态(加载/成功/错误,每类含不同数据) |
| 内存开销 | 极低,纯静态引用 | 中等,每个实例独立分配 |
三、密封接口 (sealed interface)
Kotlin 1.5 引入了 sealed interface,解决了密封类不能实现多继承的问题。一个类可以实现多个密封接口,同时享受穷举检查的红利。
// 定义两个密封接口
sealed interface ClickableEvent {
data class SingleClick(val x: Float, val y: Float) : ClickableEvent
data class DoubleClick(val x: Float, val y: Float) : ClickableEvent
}
sealed interface SwipeableEvent {
data class SwipeLeft(val distance: Int) : SwipeableEvent
data class SwipeRight(val distance: Int) : SwipeableEvent
}
// 事件类可以同时实现多个密封接口
data class ComplexGesture(
val click: ClickableEvent,
val swipe: SwipeableEvent
) : ClickableEvent, SwipeableEvent
设计陷阱: 密封接口虽然灵活,但在 when 穷举时要求必须覆盖所有子类型,如果子类型分布在不同的文件中,容易遗漏。建议为每个密封接口配套集中的 when 扩展函数。
四、编译器穷举检查
穷举检查是密封类在事件总线场景中最具价值的特性。当使用 when 表达式处理密封类或密封接口时,编译器会强制检查所有分支是否都已覆盖,无需编写 else 分支。
fun handleState(state: UiState): String {
return when (state) {
is UiState.Loading -> "加载中,进度:${state.progress}"
is UiState.Success -> "成功,数据量:${state.data.size}"
is UiState.Error -> "失败:${state.message} (${state.code})"
UiState.Empty -> "空状态"
// 如果此处新增了 Timeout 子类,编译器会报错!
}
}
// 编译器确保:新增子类 -> 编译错误 -> 强制处理
迁移收益: 传统 EventBus(如 GreenRobot / LiveDataBus)使用 String 标签或 unsafe 类型转换,发送一个未注册的事件仅会在运行时崩溃。使用密封类 + 穷举检查,所有事件类型在编译期就被强制覆盖,从架构上消除“未处理事件”风险。
五、密封类层次结构全景图
以下 SVG 展示了密封类家族的核心谱系、子类实例化差异以及编译器穷举检查的工作流:
六、实战要点总结
- 状态 vs 事件: 简单状态用 enum,携带数据的复杂状态用 sealed class;事件总线通信统一使用 sealed class / sealed interface。
- 穷举保护: 务必使用
when表达式(而非when语句)处理密封类,编译器会强制要求覆盖所有子类型。 - 模块拆分: 大型项目建议按功能模块声明密封接口,避免单个密封类膨胀到数百行子类。
- 向后兼容: 在密封类中添加新子类属于破坏性变更,建议配合版本号或 deprecated 注解平滑迁移。
面试高频题: sealed class 和 enum 在 JVM 上的实现区别?
枚举继承自 Enum 类,每个常量是静态内部类实例;密封类本质是抽象类,子类被编译为独立的 class 文件,通过继承实现多态。
枚举继承自 Enum 类,每个常量是静态内部类实例;密封类本质是抽象类,子类被编译为独立的 class 文件,通过继承实现多态。