密封类与when表达式:穷尽性检查、智能类型转换与分支处理

聊到密封类,就绕不开 when 表达式。这两个东西放在一起,才真正发挥出 Kotlin 类型系统的威力。我个人觉得,这是 Kotlin 相比 Java 最让人舒服的设计之一。

when 表达式的穷尽性检查

先问一个问题:你写 when 的时候,有没有漏掉某个分支?

在 Java 里,switch 漏掉一个 case,编译器不会管你。运行时没匹配到,就抛异常。但在 Kotlin 里,如果你用密封类作为 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("加载中...")
    }
    // 如果少写一个分支,编译器会报错
}

我在项目中遇到过好几次这样的场景:团队新同学加了一个新的密封类子类型,但忘了更新所有 when 分支。结果编译直接报红,根本跑不起来。这比运行时才发现问题,省了太多 debug 时间。

核心要点: 密封类的穷尽性检查是编译期的安全网。你永远不用担心漏掉某个分支。

智能类型转换

密封类配合 when 的另一个爽点是智能类型转换。说白了,就是编译器帮你自动做了类型强转。

fun process(result: Result) {
    when (result) {
        is Result.Success -> {
            // 这里 result 已经被智能转换为 Success
            println("数据长度: ${result.data.length}")
        }
        is Result.Error -> {
            // 这里 result 被转换为 Error
            println("错误信息: ${result.message.uppercase()}")
        }
        is Result.Loading -> {
            // 这里 result 就是 Loading 对象
            println("请稍候...")
        }
    }
}

你想想看,在 Java 里你要写 ((Result.Success) result).data,又长又容易出错。Kotlin 直接帮你省了这一步。我记得刚开始用 Kotlin 时,还习惯性地想手动强转,后来发现完全没必要。

小技巧: 智能类型转换不仅适用于密封类,也适用于所有使用 is 检查后的变量。但密封类因为子类型有限,转换起来更安全、更自然。

分支处理的最佳实践

光会用还不够,怎么写得优雅才是关键。我总结了几条实战经验。

1. 优先使用 when 而非 if-else 链

有些人习惯写 if (result is Success) ... else if (result is Error) ...。我个人建议,只要遇到密封类,一律用 when。为什么?因为 when 能利用穷尽性检查,而 if-else 不行。

// ❌ 不推荐
fun bad(result: Result) {
    if (result is Result.Success) {
        // ...
    } else if (result is Result.Error) {
        // ...
    } else {
        // 这里如果忘了处理 Loading,编译器不会报错
    }
}

// ✅ 推荐
fun good(result: Result) {
    when (result) {
        is Result.Success -> { /* ... */ }
        is Result.Error -> { /* ... */ }
        is Result.Loading -> { /* ... */ }
    }
}

2. 用 else 分支兜底?尽量别用

有些同学喜欢在 when 最后加一个 else 分支。但如果你用了密封类,加 else 反而会破坏穷尽性检查。我曾经在代码 review 时看到有人写了 else -> {},结果后来新增了一个子类型,编译器没报错,线上出了 bug。

避坑指南: 密封类的 when 里,尽量不要写 else 分支。让编译器帮你兜底,而不是自己写一个空实现。

3. 分支逻辑复杂时,提取成独立函数

如果某个分支的处理逻辑超过 5 行,我建议把它提取成一个单独的函数。这样 when 表达式保持清晰,每个分支只做一件事。

fun handleResult(result: Result) {
    when (result) {
        is Result.Success -> handleSuccess(result)
        is Result.Error -> handleError(result)
        is Result.Loading -> showLoading()
    }
}

private fun handleSuccess(success: Result.Success) {
    // 复杂的业务逻辑
    val processed = processData(success.data)
    saveToDatabase(processed)
    updateUI(processed)
}

private fun handleError(error: Result.Error) {
    // 错误处理逻辑
    logError(error.message)
    showErrorDialog(error.message)
}

private fun showLoading() {
    // 加载状态处理
    showProgressBar()
}

4. 利用 when 的返回值

when 在 Kotlin 里是表达式,不是语句。这意味着它可以有返回值。这个特性配合密封类特别实用。

fun getDisplayText(result: Result): String {
    return when (result) {
        is Result.Success -> "操作成功: ${result.data}"
        is Result.Error -> "操作失败: ${result.message}"
        is Result.Loading -> "正在处理..."
    }
}

嗯,这里要注意:如果 when 作为表达式使用,穷尽性检查就更加重要了。因为如果漏掉分支,编译器会直接报错,而不是返回 null 或默认值。

总结一下

密封类 + when 表达式,说白了就是 Kotlin 给状态管理提供的一把利器。穷尽性检查让你少犯错,智能类型转换让你少写代码,分支处理的最佳实践让你的代码更干净。

我在多个项目里都用这套模式来管理 UI 状态、网络请求结果、用户操作事件等。效果非常好,代码的可维护性明显提升。如果你还没用起来,我建议下次重构时试试看。