Kotlin 密封类:sealed class 与 when 表达式
Kotlin 中的 sealed class(密封类)是一种用于表示受限类继承层次结构的强大工具。它结合了枚举(enum)的类型安全性和抽象类的灵活性,非常适合处理状态管理、UI 渲染或结果传递等场景。配合 when 表达式使用时,编译器能够自动检查分支的完整性,从而避免潜在的逻辑漏洞。
1. 定义密封类结构
密封类的主要作用是限制继承。所有的子类必须声明在密封类所在的同一文件中(或在密封类的嵌套声明中)。这种约束使得编译器能够确定所有可能的子类类型。
创建一个新的 Kotlin 文件,例如 UiState.kt。
输入以下代码定义一个基本的密封类层级结构:
// 定义基类,使用 sealed 关键字修饰
sealed class UiState
// 定义子类,继承自 UiState
data class Success(val data: String) : UiState()
// 定义子类,用于表示错误状态
data class Error(val exception: Exception) : UiState()
// 定义子类,用于表示加载中,使用 object 因为它不需要携带数据
object Loading : UiState()
注意:在上面的代码中,Success 和 Error 携带了不同的数据,而 Loading 是一个单例。这种混合使用 data class 和 object 的方式是密封类的一大优势。
2. 在 when 表达式中进行穷尽检查
使用普通类或接口配合 when 表达式时,通常需要 else 分支来处理未知情况。而使用密封类时,如果 when 分支覆盖了所有子类,编译器会认为该表达式是完整的,从而省略 else 分支。
编写一个处理 UI 状态的函数:
fun handleUiState(state: UiState) {
// 直接使用 when 表达式
val message = when (state) {
// 编译器自动识别类型并进行智能转换
is Success -> "数据加载成功: ${state.data}"
is Error -> "发生错误: ${state.exception.message}"
// 处理 object 对象
Loading -> "正在加载中..."
// 如果此处删除 Loading 分支,编译器会报错,提示并非所有分支都被覆盖
}
// 打印结果
println(message)
}
观察代码逻辑:在 is Success 分支中,state 被自动转换为 Success 类型,因此可以直接访问 state.data 属性,无需额外的类型转换。由于 UiState 是密封的,且代码中列出了所有可能的子类(Success, Error, Loading),因此不需要编写 else 分支。
3. 对比不同方案的优劣
为了更清晰地理解密封类的适用场景,我们需要将其与常用的枚举类和普通抽象类进行对比。
| 特性 | 枚举 | 密封类 | 普通抽象类/接口 |
|---|---|---|---|
| 实例数量 | 每个常量是单例,数量有限 | 每个子类可创建多个实例 | 不限制 |
| 状态持有 | 每个实例只能有一组固定属性 | 每个子类可拥有不同类型和数量的属性 | 每个子类可拥有不同属性 |
| 继承层级 | 无层级结构(扁平化) | 支持层级结构(可嵌套) | 支持层级结构 |
| when 表达式 | 支持穷尽检查(需导入或定义在同一处) | 支持穷尽检查 | 不支持强制穷尽检查(需 else) |
4. 构建复杂的层级结构
密封类支持嵌套定义,这允许构建更加复杂、逻辑分层的状态模型。
重构之前的代码,将网络状态独立封装:
sealed class NetworkResult
// 在 NetworkResult 内部嵌套定义 Success
sealed class Success : NetworkResult() {
// 具体的成功状态,携带数据
data class Data(val content: String) : Success()
// 无数据的成功状态(例如 204 No Content)
object NoContent : Success()
}
// 定义失败状态
data class Failure(val errorCode: Int, val message: String) : NetworkResult()
// 定义进行中状态
object InProgress : NetworkResult()
处理这种嵌套结构时,when 表达式依然能保持清晰的逻辑:
fun processResult(result: NetworkResult) {
when (result) {
// 注意:这里只需要匹配外层的具体类型
is Success.Data -> println("处理数据: ${result.content}")
is Success.NoContent -> println("操作成功,无返回内容")
is Failure -> println("错误 ${result.errorCode}: ${result.message}")
is InProgress -> println("请求发送中...")
// 如果不处理 Success.NoContent,编译器会警告该分支未被覆盖
}
}
```
为了直观展示这种继承关系,我们可以使用类图来描述:
```mermaid
classDiagram
class NetworkResult {
<<sealed>>
}
class Success {
<<sealed>>
}
class Success_Data {
+String content
}
class Success_NoContent {
<<object>>
}
class Failure {
+int errorCode
+String message
}
class InProgress {
<<object>>
}
NetworkResult <|-- Success
NetworkResult <|-- Failure
NetworkResult <|-- InProgress
Success <|-- Success_Data
Success <|-- Success_NoContent
```
---
### 5. 实战:处理 RecyclerView 的多种视图类型
在实际开发中,`RecyclerView` 经常需要展示不同类型的列表项。利用密封类可以优雅地封装每种视图类型对应的数据和逻辑。
**定义**视图数据的基类:
```kotlin
sealed class RecyclerViewItem
// 文本类型的数据
data class TextItem(val title: String, val description: String) : RecyclerViewItem()
// 图片类型的数据
data class ImageItem(val imageUrl: String, val caption: String) : RecyclerViewItem()
// 广告位的数据
data class AdItem(val adId: String, val sponsorName: String) : RecyclerViewItem()
```
**创建** ViewHolder 时的匹配逻辑:
```kotlin
fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
// 实际开发中通常使用整数作为 viewType,这里演示逻辑映射
return when (viewType) {
TEXT_TYPE -> TextViewHolder(...)
IMAGE_TYPE -> ImageViewHolder(...)
AD_TYPE -> AdViewHolder(...)
else -> throw IllegalArgumentException("未知的视图类型")
}
}
fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: RecyclerViewItem) {
// 根据数据类型执行不同的绑定逻辑
when (item) {
is TextItem -> {
// 智能转换 holder 为 TextViewHolder (前提是你有自定义 holder 类型检查或复用机制)
// 这里演示数据处理的简便性
println("绑定文本: ${item.title}")
}
is ImageItem -> {
println("加载图片: ${item.imageUrl}")
}
is AdItem -> {
println("展示广告: ${item.sponsorName}")
}
}
}
通过这种方式,将数据模型与渲染逻辑强关联,代码的可读性和维护性都得到了显著提升。当新增一种列表项类型时,只需继承 RecyclerViewItem 并在 when 表达式中补充对应的处理逻辑,编译器会自动引导你完成所有必须的修改。

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