Skip to content

密封类与接口 (Sealed Classes)

源: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

为什么需要密封接口?

  1. 多继承:类只能继承一个父类,但可以实现多个接口。如果你的数据类已经继承了其他类(例如 View),它就无法再继承 sealed class,但可以实现 sealed interface
  2. 更少的层级:不需要像密封类那样维护构造函数。
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 之前,打印 objecttoString() 结果是不直观的类哈希码。

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)
    }
}

限制与规则

  1. 包限制:密封类的所有直接子类必须定义在同一个包 (Package) 中。
  2. 模块限制:所有子类必须在同一个模块 (Module) 中编译。这意味着你不能在库的外部定义其密封类的子类(这正是“密封”的含义)。
  3. 构造函数:密封类的构造函数默认是 protected 的(且只能是 protectedprivate)。

总结

  • 状态机首选:任何涉及“有限状态集合”的场景(加载/成功/失败),首选密封类。
  • 编译器保镖:利用 when 的完备性检查,将运行时错误提前到编译期。
  • 接口灵活:优先考虑 sealed interface,除非你需要从父类继承代码。
  • 数据对象:用 data object 替代普通的 object 以获得更好的日志体验。