Kotlin sealed interface配合when表达式实现穷举式匹配
使用 sealed interface 与 when 表达式配合,可以在编译期强制处理所有可能的情况,将运行时错误转化为编译错误,极大提升代码的健壮性与可维护性。
第一步:理解核心问题与解决方案
在处理多种类型或状态时,例如网络请求的结果(成功、错误、加载中)、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)
}
-
触发编译时检查。假设我们新增一个状态,但忘记在
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状态。一个网络请求驱动的页面可能有加载、成功、错误、以及无数据等状态。
-
定义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> } -
在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) } } } -
在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() // 编译器保证覆盖了所有可能 } }
第五步:最佳实践与注意事项
-
优先使用
sealed interface。相较于sealed class,sealed interface允许一个类实现多个密封层次,提供了更大的灵活性。除非你有必须使用构造函数或继承的语义理由,否则推荐使用sealed interface。 -
将所有子类型定义在同一个文件中。这是
sealed interface的基本规则,也是编译器能进行穷尽性检查的前提。通常将密封接口及其所有直接子类放在一个名为XxxResponse.kt或UiState.kt的文件中。 -
结合
data class和object。对于需要携带不同数据的状态,使用data class,它自动生成equals()、hashCode()、toString()等方法。对于状态本身是单例或不携带额外数据的情况(如Loading),使用object。 -
在
when中不需要else。穷尽性检查已经隐含了else的作用。添加else分支会掩盖编译器的检查,反而降低了代码安全性,应避免。 -
与表达式结合。
when在 Kotlin 中是一个表达式,可以返回一个值,这让它非常适合用于状态到结果的转换逻辑,如上面handleResponse函数中的用法,代码更简洁。

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