第068篇 sealed class:状态建模的利器

简介: `sealed class` 是 Kotlin 状态建模的核心工具,本质是“用类型系统表达封闭状态机”。它强制子类同文件/模块定义,使编译器可穷举所有分支,配合 `when` 实现**无 `else` 的完备匹配与类型安全**,彻底杜绝非法状态(如 `loading && data != null`),远超枚举与多字段布尔组合的表达力。

sealed class 在 Kotlin 里的地位,相当于"用类型系统表达状态机"。它的价值不在语法,而在一个很实际的痛点:当一个值有多种可能形态时,怎么保证"处理了这几种形态"这件事不会漏。 面试里问 sealed class,问的往往不是"是什么",而是"你用它解决过什么建模问题"和"它和枚举、when 表达式的关系"。

先把结论放在前面:sealed class(1.5 起可写 sealed interface) restricts 子类必须与父类同一文件内声明(1.5 后放宽到同一包/模块的编译单元),这让子类型集合是封闭且可穷举的。于是 when 表达式在 Kotlin 里被当作表达式处理——编译器知道所有分支,漏一个就编译报错,同时 when 有返回值。 核心用途是显式建模互斥状态,替代"多个可空字段 + 若干布尔标记"这种做法。

机制拆解

先说实现机制。sealed 类在字节码层面是 abstract 类加一个 PermittedSubclasses 属性(JVM 的 sealed 类特性),子类编译后带上 sealed 标记。 运行时子类集合是可枚举的,所以 Kotlin 编译器才能在 when 里做穷举性检查——如果一个 when 覆盖了所有已知子类且没有 else 分支,它被视为完整表达式,有返回值;漏掉任何一个子类,编译直接报错。 这就是它与枚举的本质差别:枚举的成员集合在编译期固定,sealed class 的成员集合在编译期可检查、在运行期是真实的继承体系。

再说它解决的建模问题。典型的"半渲染状态"问题:一个页面要显示加载中/有数据/出错/空数据,如果用三个可空字段表示(loading: Boolean、data: List<T>?、error: String?),就会出现 loading = true 且 data != null 这种非法组合,UI 层只能靠散落的 if 去防御,字段一多就难免出错。 换成 sealed class 后,状态是一个值,四种情况互斥,编译器保证处理完整——这就是"把非法状态变成不可表示"。

Android 语境下的三个高频落点:① 状态建模(LoadState、UiState);② 多态事件的类型安全表达(Channel/Flow 传的事件、onActivityResult 的多种结果);③ 替代"结果 + 异常"二元返回(Result<T> 就是 sealed class 的标准实现,Success/Failure)。

这些坑的正确绕法

最常见的坑是在 when 密封类分支里加了 else,新增子类时编译器不再提示遗漏分支。 表现是某次新增一个 Empty 状态,编译照过,但没有任何分支处理它,运行期走了某个旧分支的默认逻辑,界面状态错乱。根因是 else 把"编译器帮你检查完整性"这个保障关掉了。 修法很直接:不要写 else,让编译器逼你补分支;万一确实需要兜底(比如状态来自 Java 的平台类型、可能真有未知值),用 is 分支逐个列出已知类型,只在最后对 null 做处理,而不是对任意类型做兜底。

其次是把 sealed class 用成了"枚举的替代品",丢了它真正的价值。 表现是每个子类里只有一个常量、没有任何差异化行为,写起来比枚举还长。判据是:子类之间是否需要携带不同的数据或行为。需要,就用 sealed class;不需要(只是一组常量),就用 enum class 或 object 组合。 强行用 sealed class 会得到"一堆空壳类 + 长 when",反而不如枚举。

还有一个更隐蔽的坑:sealed 的"同文件"限制在重构时容易踩。 表现是把状态类挪到另一个文件(拆分类)后编译失败,或者为了绕过限制把 sealed 去掉,于是 when 的穷举检查失效,回到第一个坑。 对策是把一个状态的所有子类放在同一个文件里(这是 Kotlin 社区的通行约定,读代码时也更容易看全),实在要拆分就用 1.5 后的 sealed interface 并放在同一模块内。

代码里见真章

看一段能直接跑的代码,把上面的机制落到具体写法上:

// 1) 反例:多个可空字段能表示出非法组合
data class BadState(
    val loading: Boolean = false,
    val data: List<Item>? = null,
    val error: String? = null
)
// BadState(loading = true, data = listOf(item))  // 编译通过,逻辑矛盾

// 2) 正解:sealed interface 建模互斥状态,非法状态不可表示
sealed interface UiState<out T> {
   
    data object Loading : UiState<Nothing>                 // 无数据
    data class Content<T>(val items: List<T>) : UiState<T> // 有数据
    data class Failed(val msg: String) : UiState<Nothing>  // 失败
}
data object Empty : UiState<Nothing>                       // 空数据(与 Loading 区分)

// 3) when 穷举检查:不写 else,漏分支编译报错
fun render(state: UiState<Item>) {
   
    when (state) {
                          // 作为表达式,有返回值
        UiState.Loading -> showSkeleton()
        is UiState.Content -> showList(state.items)
        is UiState.Failed -> showError(state.msg)
        Empty -> showBlank()
        // 新增一个状态而这里不补分支 → 编译报错,这是 sealed 的核心价值
    }
}

// 4) sealed class 也能带基类逻辑
sealed class Result2<out T> {
   
    data class Ok<T>(val value: T) : Result2<T>()
    data class Err(val code: Int, val msg: String) : Result2<Nothing>()
    // 基类可以定义统一行为
    val isOk: Boolean get() = this is Ok
}

// 5) 替代"结果 + 异常":让失败成为类型的一部分
fun load(): UiState<Item> = try {
   
    UiState.Content(repo.load())
} catch (e: IOException) {
   
    UiState.Failed(e.message ?: "网络异常")   // 失败被显式建模,不靠抛异常传控制流
}

// 6) 事件建模:多态事件也适合 sealed
sealed interface Event {
   
    data class Click(val id: String) : Event
    data class Scroll(val offset: Int) : Event
}

这段代码值得盯三处:第一处,BadState 允许构造出矛盾组合,而 UiState 从类型上排除了这种可能——这是本篇的核心论点;第二处,when 不写 else,让编译器强制补全分支,注释里明确写了"这是 sealed 的核心价值";第三处,Empty 独立成一个 data object,说明"加载中"与"加载完成但没数据"是两种不同状态,合并不了——这正是 sealed 建模比布尔字段强的地方。

这题在面试里怎么问、怎么答

"sealed class 和 enum class 有什么区别?"四点:①枚举成员集合编译期固定、sealed 允许每个子类携带不同数据;②枚举是单例、sealed 子类可以有多个实例;③sealed 支持继承层次(子类可再抽象),枚举不能;④sealed 的 when 穷举性由编译器检查(前提不写 else),枚举的 when 在有 else 时也不检查完整性。 选型判据:只是常量用 enum,要带数据用 sealed。

"sealed 为什么能穷举?"答:编译器知道所有直接子类(受"同文件/同模块"限制约束),所以能校验 when 的覆盖情况。1.5 之前限制在同文件,1.5 之后 sealed interface 可以有同包/同编译单元的实现。

"用 sealed 就不用 try-catch 了?"答:不能完全替代。sealed 解决的是"函数应当如何表达失败"这个接口设计问题——把失败写进返回类型,调用方必须处理。异常仍然存在(比如编程错误、底层库抛出),但应当用 sealed 把预期的、可预期的失败建模出来,把不该发生的留给异常。 Android 里 Coroutines 的 Result、Room 的返回值、Repository 的返回值都是这个思路。

"使用中遇到过什么问题?"案例一:状态用三个可空字段表示,加载逻辑散落各处,出现"转圈的同时显示了旧数据";定位到非法状态可表示;修复为改 sealed interface 并让 when 强制分支。案例二:when 写了 else -> {} 导致新增状态无处理,页面空白;定位到漏分支被 else 掩盖;修复为去掉 else,补齐每个分支。

再补一个工程上值得讲清的一点:sealed class 让"状态机的状态"与"状态转移的守卫条件"可以分别表达。 典型写法是:状态用 sealed 表达,转移用扩展函数表达(fun UiState<Item>.retry(): UiState<Item> = UiState.Loading)。 这样状态的取值集合与允许的转移路径都集中在一处,非法转移在 review 时就能看出来。Android 上这与单向数据流的实践一致——状态是数据,事件是意图,reducer 是纯函数,逐个可测。这类"把状态转移写成纯函数"的设计,是 sealed 能带来的最大工程收益,也是面试里最值得展开讲的一点。

给正在准备面试的你

把这题画成一张"从多字段到密封类"的迁移图:上半部分画 BadState 的三个可空字段,并在中间用红叉标出两条连线——"loading=true 且 data 非空"、"error 非空且 data 非空",表示这两种组合可被构造出来但逻辑非法。 下半部分画 UiState 的四个分支(Loading / Content / Failed / Empty),四个框互斥、用一条竖线分隔并标"编译器保证互斥"。两侧各配一条注释:"多字段:需要散落的 if 去防御" vs "密封类:非法状态不可表示"。图右边再画一个 when 方框,里面四个分支不写 else,旁边标一句"漏分支 = 编译错误"。 这张图能完整回答"你为什么用 sealed"。

再补工程案例与踩坑——应用落点是把项目里用多个可空/布尔字段表示状态的类逐个改造成 sealed interface,删掉所有 when 里的 else 分支,让编译器把遗漏点逐个暴露出来再补;状态转移统一写成扩展函数或 reducer 便于单测。

复习时别孤立刷题:data class 上——sealed 的子类通常就是 data class 或 data object,两者是"状态载体"与"状态集合"的关系。

划两句重点:sealed 的子类型封闭可穷举,when 不写 else 时编译器强制补全;多个可空字段能构造出非法组合,sealed 从类型上排除;只是一组常量用 enum,要带数据用 sealed;状态转移写成纯函数最受益。

下一篇聊 object 与 companion object:Kotlin 里的单例——沿着今天这条主线继续往前走。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:data-class:一行顶-Java-一百行

下一篇预告:object-与-companion-object:Kotlin-里的单例

有任何问题欢迎在评论区留言交流。

相关文章
|
17天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8490 24
|
16天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
2867 14
|
15天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2029 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
14天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
10天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
4天前
|
人工智能 JSON Linux
【全网最详细】ComfyUI使用教程:下载+本地部署+配置+工作流搭建一篇搞定(2026最新版)
ComfyUI是一款免费开源的本地AI绘图工具,采用节点式工作流设计,支持文生图、图生图、局部重绘、放大、换脸等多种功能。可离线运行,依赖显卡加速,无需联网。支持自定义流程保存与分享,插件生态丰富,适合进阶用户。(239字)
|
10天前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)

热门文章

最新文章