3. 密封类的核心语法:sealed class vs sealed interface

好,咱们直接进入正题。密封类这个东西,说白了就是 Kotlin 给咱们提供的一种「有限状态」的表达方式。我刚开始接触 Kotlin 时,看到 sealed class 的第一反应是:这不就是个加强版的枚举吗?后来在项目里真正用起来才发现,它比枚举要灵活得多。

3.1 什么是密封类?

密封类,顾名思义,就是「密封」起来的类。它限制了一个类的继承结构——所有子类都必须定义在同一个文件里。这样做的好处很明显:编译器知道所有可能的子类型,所以 when 表达式可以做到穷举检查。

核心要点:密封类是一种受限的类层次结构。你无法在文件外部继承它。

来看个最简单的例子:

sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val message: String) : Result()
    object Loading : Result()
}

fun handleResult(result: Result) {
    when (result) {
        is Result.Success -> println("成功: ${result.data}")
        is Result.Error -> println("失败: ${result.message}")
        is Result.Loading -> println("加载中...")
    }
    // 不需要 else 分支,因为编译器知道已经穷举了
}

嗯,这里要注意:如果没有 sealed 关键字,when 表达式就必须加 else 分支。加了 sealed,编译器会帮你检查有没有遗漏。我在项目中遇到过好几次因为新增状态忘记处理导致的 bug,用了密封类之后这类问题基本绝迹了。

3.2 sealed class 与 sealed interface 的区别

Kotlin 1.5 之后引入了 sealed interface。很多人会问:这俩到底有啥区别?我该用哪个?

说白了,区别就两点:

  • 继承方式不同:sealed class 只能被类继承(class、object),而 sealed interface 可以被类、接口、枚举实现
  • 状态携带不同:sealed class 可以有构造函数参数,sealed interface 不能有状态

我个人的习惯是:如果子类型需要携带数据,用 sealed class;如果只是标记类型,用 sealed interface。举个例子:

// 需要携带数据 → 用 sealed class
sealed class UiState {
    data class Success(val data: List<User>) : UiState()
    data class Error(val code: Int, val message: String) : UiState()
    object Loading : UiState()
}

// 只是标记类型 → 用 sealed interface
sealed interface Action {
    object Click : Action
    object LongPress : Action
    data class Swipe(val distance: Float) : Action
}

小技巧:如果你不确定用哪个,先问自己一个问题:「这些子类型需要共享构造函数参数吗?」需要就用 sealed class,不需要就用 sealed interface。

3.3 密封类的核心特性

咱们来梳理一下密封类的几个关键特性。我建议你记住这三点:

  1. 穷举性:when 表达式可以穷举所有子类型,不需要 else 分支
  2. 单文件限制:所有直接子类必须在同一个文件中定义
  3. 可带状态:密封类本身可以有构造函数参数,子类可以携带数据

你想想看,这三点组合起来意味着什么?意味着你可以用类型系统来表达状态机。每个子类型代表一个状态,编译器帮你保证所有状态都被处理了。这在 Android 的状态管理里简直是神器。

3.4 实战对比:枚举 vs 密封类

我记得刚带团队时,有个同事问我:「为什么不用枚举?枚举也能表示有限状态啊。」好问题。咱们来对比一下:

特性 枚举 密封类
携带数据 只能携带固定属性 每个子类可以携带不同数据
穷举检查 支持 支持
扩展性 无法扩展 可以在文件内扩展
内存占用 单例,内存友好 每个实例独立
适用场景 纯状态标记 状态+数据

看明白了吧?枚举适合「状态本身不需要额外数据」的场景,比如网络状态(CONNECTED、DISCONNECTED)。密封类适合「每个状态需要携带不同数据」的场景,比如网络请求结果(成功带数据、失败带错误信息)。

3.5 避坑指南

我曾经踩过的坑:在密封类里用了 data class 作为子类,然后忘了 data class 会自动生成 equals/hashCode。这在某些场景下会导致意外行为——比如两个相同数据的实例被认为不相等。如果你需要比较密封类实例,记得考虑这一点。

还有一点:密封类的子类不能是局部类或匿名类。所有子类必须是顶层类或嵌套类。这个限制其实挺好的,它强迫你把所有状态定义放在一起,代码结构更清晰。

3.6 知识体系图

下面这张图帮你理清密封类的知识脉络:

密封类/密封接口 sealed class sealed interface 穷举性 单文件限制 可带状态 状态管理(UI状态) 网络请求结果封装 事件/动作分发 核心价值:用类型系统表达有限状态,编译器保证穷举

3.7 什么时候该用什么?

最后,我总结一下我的选择原则:

  • 用 sealed class:子类型需要携带不同数据,或者需要共享构造函数参数
  • 用 sealed interface:只是标记类型,不需要共享状态,或者需要被枚举实现
  • 用枚举:状态本身不需要额外数据,且数量固定不变

嗯,其实没有绝对的对错。我在项目里经常混用——用枚举表示网络状态(CONNECTED/DISCONNECTED),用密封类表示网络请求结果(Success/Error/Loading)。关键是看你的业务场景需要什么。

个人建议:如果你刚开始用密封类,先从简单的场景入手——比如把网络请求结果改成密封类。用熟了之后,你会发现它几乎可以替代所有「有限状态+数据」的场景。

好了,密封类的核心语法就讲到这里。记住一句话:密封类不是银弹,但在状态管理这个领域,它确实是最趁手的工具之一。


公众号:蓝海资料掘金营,微信deep3321