Kotlin 中密封类(Sealed Class)的主要用途之一是实现"穷尽性检查"(Exhaustiveness Checking) 。这是它区别于普通抽象类或接口的关键优势。下面我们详细解释:
🔍 什么是"穷尽性检查"?
穷尽性检查是指:
编译器能够静态地验证你在处理一个封闭类型(如密封类)时,是否已经覆盖了所有可能的子类型。
如果遗漏了某个子类型,编译器会报错,从而避免运行时逻辑错误。
🧩 为什么需要穷尽性检查?
考虑一个常见的场景:处理操作结果(成功、失败、加载中......)。如果你用普通父类:
Kotlin
1open class Result
2class Success(val data: String) : Result()
3class Error(val msg: String) : Result()
然后用 when 处理:
scss
Kotlin
编辑
1fun handle(result: Result) = when (result) {
2 is Success -> println("OK")
3 // 忘记处理 Error!
4}
✅ 这段代码能编译通过!
但运行时如果传入 Error,就会抛出 kotlin.NoWhenBranchMatchedException ------ 这是一个运行时错误!
而使用密封类 ,就能在编译期发现这个问题。
✅ 密封类如何实现穷尽性检查?
示例:用密封类定义结果
kotlin
Kotlin
编辑
1sealed class Result {
2 data class Success(val data: String) : Result()
3 data class Error(val msg: String) : Result()
4 object Loading : Result()
5}
使用 when 处理(不带 else)
rust
Kotlin
编辑
1fun handle(result: Result): String = when (result) {
2 is Result.Success -> "成功: ${result.data}"
3 is Result.Error -> "失败: ${result.msg}"
4 Result.Loading -> "加载中"
5 // 所有分支都覆盖了 → 编译通过 ✅
6}
现在,如果你删除任意一个分支 ,比如注释掉 Loading:
rust
Kotlin
编辑
1fun handle(result: Result): String = when (result) {
2 is Result.Success -> "成功"
3 is Result.Error -> "失败"
4 // ❌ 缺少 Loading 分支!
5}
👉 编译器会报错:
csharp
Text
编辑
1'when' expression must be exhaustive, add necessary 'Result.Loading' branch or 'else' branch instead
💡 这就是穷尽性检查 :编译器知道
Result只有这三种子类型,并强制你处理全部。
🛑 如果加了 else 会怎样?
rust
Kotlin
编辑
1fun handle(result: Result): String = when (result) {
2 is Result.Success -> "成功"
3 else -> "其他情况"
4}
✅ 这样也能编译通过,因为 else 被视为"兜底"。
但失去了类型安全的优势!
- 如果以后新增一个子类(比如
Retry),你的else会默默吞掉它,可能引发逻辑 bug。 - 而如果不写
else,新增子类后所有未更新的when都会编译失败,迫使你显式处理新情况。
✅ 最佳实践:尽量不写
else,让编译器帮你检查完整性。
📌 穷尽性检查的前提条件
-
when作为表达式使用(即有返回值或赋值给变量)csharpKotlin 编辑 1val message = when (result) { ... } // ✅ 会触发穷尽性检查如果只是语句(无返回值),且所有分支都有
return/throw,也会检查;否则可能不检查。 -
类型必须是密封类或其子类型
普通类、接口、开放类(
open class)不会触发穷尽性检查。 -
所有直接子类必须在编译期可知
密封类的子类必须在同一文件(或同一模块内),这样编译器才能"看到全部可能性"。
🎯 实际价值:重构安全 + 逻辑可靠
假设你后期给 Result 新增一个状态:
Kotlin
编辑
1sealed class Result {
2 // ... 原有
3 object Retry : Result() // 新增
4}
那么所有使用 when (result) 的地方都会编译失败 ,除非你显式处理 Retry。
这让你在扩展功能时不会遗漏任何调用点,极大提升代码健壮性。
✅ 总结:穷尽性检查的核心意义
| 特性 | 普通类/接口 | 密封类 |
|---|---|---|
| 子类型数量 | 开放(可无限扩展) | 封闭(编译期已知) |
when 是否强制覆盖所有子类 |
❌ 否 | ✅ 是(作为表达式时) |
| 新增子类是否影响现有代码 | ❌ 不会报错(可能出 bug) | ✅ 编译失败(强制修复) |
| 适合场景 | 开放扩展 | 封闭状态机、结果封装等 |
🔑 密封类 +
when表达式 = 编译期保证逻辑完整性
这就是为什么 Kotlin 官方推荐:当你有一组固定的、相关的类型时,优先使用密封类而不是普通继承。