密封类基础回顾: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 展示了密封类家族的核心谱系、子类实例化差异以及编译器穷举检查的工作流:

密封类(sealed class)与编译器穷举检查 sealed class UiState data class Loading(val progress: Int) data class Error(val msg: String) object Empty sealed interface Event class ClickEvent : Event class SwipeEvent : Event class LongPressEvent : Event when (event) { ... } 编译器穷举检查 强制覆盖所有子类分支,无需 else → 新增子类触发编译错误 运行时崩溃 → 编译时失败,架构级安全保障 运行时崩溃 编译时失败

六、实战要点总结

  • 状态 vs 事件: 简单状态用 enum,携带数据的复杂状态用 sealed class;事件总线通信统一使用 sealed class / sealed interface。
  • 穷举保护: 务必使用 when 表达式(而非 when 语句)处理密封类,编译器会强制要求覆盖所有子类型。
  • 模块拆分: 大型项目建议按功能模块声明密封接口,避免单个密封类膨胀到数百行子类。
  • 向后兼容: 在密封类中添加新子类属于破坏性变更,建议配合版本号或 deprecated 注解平滑迁移。
面试高频题: sealed class 和 enum 在 JVM 上的实现区别?
枚举继承自 Enum 类,每个常量是静态内部类实例;密封类本质是抽象类,子类被编译为独立的 class 文件,通过继承实现多态。