文章目录

Kotlin sealed interface配合when表达式实现穷举式匹配

发布于 2026-06-04 12:48:07 · 浏览 13 次 · 评论 0 条

Kotlin sealed interface配合when表达式实现穷举式匹配

使用 sealed interfacewhen 表达式配合,可以在编译期强制处理所有可能的情况,将运行时错误转化为编译错误,极大提升代码的健壮性与可维护性。


第一步:理解核心问题与解决方案

在处理多种类型或状态时,例如网络请求的结果(成功、错误、加载中)、UI的状态(正常、错误、空数据)、或者复杂的业务模型,我们通常会使用一个公共的父类型,然后用 when 表达式来分别处理每个子类型。

interface ApiResponse
class Success(val data: String) : ApiResponse
class Error(val code: Int, val message: String) : ApiResponse
object Loading : ApiResponse

fun handleResponse(response: ApiResponse) {
    when (response) {
        is Success -> println("数据: ${response.data}")
        is Error -> println("错误: ${response.code} - ${response.message}")
        // 如果遗漏了对 Loading 的处理,代码依然可以编译通过,这是一个潜在的BUG源。
    }
}
```

**问题在于**:编译器无法知道 `ApiResponse` 的所有可能实现。如果你在 `when` 表达式中遗漏了一个分支(例如 `Loading`),程序依然能编译通过,直到运行时可能引发不可预料的问题。

**解决方案是**:使用 **密封接口**。它明确地定义了一个受限的继承层次结构,编译器知晓所有可能的子类型,从而可以在 `when` 表达式中强制实现 **穷举式匹配**。

---

## 第二步:定义密封接口

密封接口在普通接口前加上 `sealed` 关键字。它的所有直接实现必须定义在同一个文件中。

1.  **定义**密封接口作为公共父类型。

    ```kotlin
    sealed interface ApiResponse
    ```

2.  **定义**所有可能的子类型。

    ```kotlin
    // 使用 data class 表示包含具体数据的状态
    data class Success(val data: String) : ApiResponse
    data class Error(val code: Int, val message: String) : ApiResponse
    // 使用 object 表示单一状态或没有额外数据的状态
    object Loading : ApiResponse
    ```

**关键特性**:`sealed interface` 本身是抽象的,不能直接实例化。其子类型可以是 `class`、`data class`、`object`,甚至是另一个 `sealed interface`,从而形成更复杂的层次。

---

## 第三步:与`when`表达式配合实现穷举

当 `when` 表达式的参数是 `sealed interface` 的类型时,编译器会检查 `when` 分支是否覆盖了所有可能的子类型。

1.  **使用** `when` **表达式处理** `ApiResponse`。

    ```kotlin
    fun handleResponse(response: ApiResponse) {
        // 此处 `response` 是 ApiResponse 类型,它是一个 sealed interface
        val result = when (response) {
            is Success -> "成功获取数据: ${response.data}"
            is Error -> "发生错误 [${response.code}]: ${response.message}"
            is Loading -> "正在加载中..."
            // 不再需要 else 分支!
            // 如果尝试添加 `else ->`,编译器会警告这个分支永远不会被执行,因为情况已被穷举。
        }
        println(result)
    }
  1. 触发编译时检查。假设我们新增一个状态,但忘记在 when 中处理它。

    // 在 ApiResponse.kt 文件中新增一个子类
    data class Cached(val data: String, val timestamp: Long) : ApiResponse
    
    fun handleResponse(response: ApiResponse) {
        val result = when (response) {
            is Success -> "..."
            is Error -> "..."
            is Loading -> "..."
            // 缺少对 `is Cached` 的处理
        }
    }

    编译器会立即报错:'when' expression must be exhaustive, add necessary 'is Cached' branch or 'else' branch instead.。这迫使开发者必须处理新的类型,避免了运行时因遗漏分支而导致的意外


第四步:在实际场景中的应用

场景:建模复杂的UI状态。一个网络请求驱动的页面可能有加载、成功、错误、以及无数据等状态。

  1. 定义UI状态的密封接口。

    sealed interface UiState<out T> {
        object Loading : UiState<Nothing>
        data class Success<T>(val data: T) : UiState<T>
        data class Error(val throwable: Throwable) : UiState<Nothing>
        object Empty : UiState<Nothing>
    }
  2. 在ViewModel或Presenter中更新状态

    class MyViewModel {
        var uiState: UiState<List<User>> = UiState.Loading
            private set
    
        fun fetchUsers() {
            uiState = UiState.Loading
            // 模拟网络请求
            try {
                val users = networkService.getUsers()
                uiState = if (users.isEmpty()) {
                    UiState.Empty
                } else {
                    UiState.Success(users)
                }
            } catch (e: Exception) {
                uiState = UiState.Error(e)
            }
        }
    }
  3. 在View层或UI组件中渲染。使用 when 安全地处理每一种状态。

    fun renderUiState(state: UiState<List<User>>) {
        when (state) {
            is UiState.Loading -> showLoadingSpinner()
            is UiState.Success -> displayUsers(state.data)
            is UiState.Error -> showError(state.throwable.message)
            is UiState.Empty -> showEmptyView()
            // 编译器保证覆盖了所有可能
        }
    }

第五步:最佳实践与注意事项

  1. 优先使用 sealed interface。相较于 sealed classsealed interface 允许一个类实现多个密封层次,提供了更大的灵活性。除非你有必须使用构造函数或继承的语义理由,否则推荐使用 sealed interface

  2. 将所有子类型定义在同一个文件中。这是 sealed interface 的基本规则,也是编译器能进行穷尽性检查的前提。通常将密封接口及其所有直接子类放在一个名为 XxxResponse.ktUiState.kt 的文件中。

  3. 结合 data classobject。对于需要携带不同数据的状态,使用 data class,它自动生成 equals()hashCode()toString() 等方法。对于状态本身是单例或不携带额外数据的情况(如 Loading),使用 object

  4. when 中不需要 else。穷尽性检查已经隐含了 else 的作用。添加 else 分支会掩盖编译器的检查,反而降低了代码安全性,应避免。

  5. 与表达式结合when 在 Kotlin 中是一个表达式,可以返回一个值,这让它非常适合用于状态到结果的转换逻辑,如上面 handleResponse 函数中的用法,代码更简洁。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文