密封类与接口 (Sealed Classes)
在建模业务逻辑时,我们经常遇到“某个值只能是有限集合中的一种”的情况。枚举(Enum)能解决简单的问题,但如果每个状态都需要携带不同的数据,枚举就无能为力了。
密封类 (Sealed Class) 就是为了解决这个问题而生的。它表示一个受限的类层级结构,是“代数数据类型 (ADT)”在 Kotlin 中的实现。
密封类 vs 枚举
| 特性 | Enum (枚举) | Sealed Class (密封类) |
|---|---|---|
| 实例数量 | 每个枚举常量是单例 | 子类可以有多个实例 |
| 状态携带 | 所有常量结构相同 | 每个子类可拥有完全不同的属性 |
| 继承关系 | 无法继承 | 支持完整的类继承体系 |
基础用法
使用 sealed 关键字修饰类。其所有直接子类在编译时必须已知。
kotlin
sealed class UIState {
// 纯单例状态:不需要携带数据
data object Loading : UIState()
// 数据状态:携带列表
data class Success(val items: List<String>) : UIState()
// 错误状态:携带异常信息
data class Error(val exception: Throwable) : UIState()
}完备性检查 (Exhaustiveness) 核心价值
当在 when 表达式中使用密封类时,编译器会强制检查是否覆盖了所有可能的子类。这意味着你不需要写 else 分支。
kotlin
fun updateUI(state: UIState) = when (state) {
is UIState.Loading -> showSpinner()
is UIState.Success -> showList(state.items) // 智能转换
is UIState.Error -> showError(state.exception)
// 无需 else。如果未来新增了 UIState.Empty,编译器会立刻报错提醒
}密封接口 (Sealed Interfaces) Kotlin 1.5+
除了密封类,Kotlin 还引入了 sealed interface。
为什么需要密封接口?
- 多继承:类只能继承一个父类,但可以实现多个接口。如果你的数据类已经继承了其他类(例如
View),它就无法再继承sealed class,但可以实现sealed interface。 - 更少的层级:不需要像密封类那样维护构造函数。
kotlin
sealed interface Error
sealed interface Progress
// 这个类同时属于 Error 体系和 Progress 体系
class Timeout(val time: Long) : Error, Progress数据对象 (Data Objects) Kotlin 1.9+
在密封类中,对于没有属性的单例状态,我们通常使用 object。但在 Kotlin 1.9 之前,打印 object 的 toString() 结果是不直观的类哈希码。
Kotlin 1.9 引入了 data object,它为单例对象生成友好的 toString()。
kotlin
sealed class Result {
data object Loading : Result() // toString() -> "Loading"
}实战:MVI 架构中的应用
在现代 Android 架构(MVI)中,密封类是定义 UiState (状态) 和 UiEvent (事件) 的标准方式。
kotlin
// 定义用户意图 (Intent/Event)
sealed interface UserIntent {
data object Refresh : UserIntent
data class ToggleLike(val id: Int) : UserIntent
data class RemoveItem(val id: Int) : UserIntent
}
// ViewModel 处理
fun handleIntent(intent: UserIntent) {
when (intent) {
UserIntent.Refresh -> loadData()
is UserIntent.ToggleLike -> repo.like(intent.id)
is UserIntent.RemoveItem -> repo.remove(intent.id)
}
}限制与规则
- 包限制:密封类的所有直接子类必须定义在同一个包 (Package) 中。
- 模块限制:所有子类必须在同一个模块 (Module) 中编译。这意味着你不能在库的外部定义其密封类的子类(这正是“密封”的含义)。
- 构造函数:密封类的构造函数默认是
protected的(且只能是protected或private)。
总结
- 状态机首选:任何涉及“有限状态集合”的场景(加载/成功/失败),首选密封类。
- 编译器保镖:利用
when的完备性检查,将运行时错误提前到编译期。 - 接口灵活:优先考虑
sealed interface,除非你需要从父类继承代码。 - 数据对象:用
data object替代普通的object以获得更好的日志体验。