密封类与密封接口
Kotlin密封类与密封接口详解:受限继承与穷举检查。
概述
密封类(sealed class)和密封接口(sealed interface)限制继承范围,所有子类必须在同一文件或同一包中声明。编译器可以穷举所有子类型,配合 when 表达式实现类型安全的分支处理,是建模有限状态和代数数据类型的利器。
基础概念
密封类 vs 枚举
| 特性 | 密封类 | 枚举 |
|---|---|---|
| 实例数量 | 每个子类可有多个实例 | 每个值只有一个实例 |
| 状态携带 | 子类可携带不同类型的数据 | 所有值共享相同属性 |
| 继承限制 | 限定在同一文件/包 | 不可继承 |
| 类型检查 | when 穷举检查 | when 穷举检查 |
密封类的特点
- 子类必须在同一文件中声明(Kotlin 1.5+ 允许同一包中不同文件)
- 子类可以是 data class、object 或普通 class
- 密封类本身是抽象的,不能直接实例化
- 配合 when 表达式,编译器会检查是否穷举所有分支
快速上手
密封类基本用法
// 定义密封类表示网络请求结果
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// when 表达式穷举所有子类,无需 else 分支
fun handle(result: Result<Int>) = when (result) {
is Result.Success -> println("成功: ${result.value}")
is Result.Error -> println("错误: ${result.message}")
Result.Loading -> println("加载中...")
// 编译器保证覆盖所有情况,不需要 else
}
密封接口基本用法
// 密封接口:允许多继承
sealed interface Action {
data class Click(val x: Int, val y: Int) : Action
data class Swipe(val direction: Direction) : Action
object Idle : Action
}
// 密封接口可以组合
sealed interface Drawable {
fun draw()
}
sealed interface Clickable {
fun onClick()
}
// 同时实现两个密封接口
data class Button(val label: String) : Drawable, Clickable {
override fun draw() { /* 绘制按钮 */ }
override fun onClick() { /* 处理点击 */ }
}
详细用法
嵌套密封类
// 嵌套密封类构建层级结构
sealed class UiState {
object Loading : UiState()
sealed class Content : UiState() {
data class UserList(val users: List<User>) : Content()
data class UserDetail(val user: User) : Content()
}
sealed class Error : UiState() {
data class Network(val code: Int) : Error()
data class Auth(val reason: String) : Error()
}
}
// 分层处理
fun render(state: UiState) = when (state) {
is UiState.Loading -> showLoading()
is UiState.Content.UserList -> showUserList(state.users)
is UiState.Content.UserDetail -> showUserDetail(state.user)
is UiState.Error.Network -> showNetworkError(state.code)
is UiState.Error.Auth -> showAuthError(state.reason)
}
密封类与递归结构
// 使用密封类建模表达式树(递归结构)
sealed class Expr {
data class Const(val value: Double) : Expr()
data class Sum(val left: Expr, val right: Expr) : Expr()
data class Mul(val left: Expr, val right: Expr) : Expr()
data class Neg(val expr: Expr) : Expr()
}
// 递归求值
fun eval(expr: Expr): Double = when (expr) {
is Expr.Const -> expr.value
is Expr.Sum -> eval(expr.left) + eval(expr.right)
is Expr.Mul -> eval(expr.left) * eval(expr.right)
is Expr.Neg -> -eval(expr.expr)
}
// 构建表达式
val expression = Expr.Sum(
Expr.Const(1.0),
Expr.Mul(Expr.Const(2.0), Expr.Const(3.0))
)
println(eval(expression)) // 7.0
密封类与模式匹配
// 配合智能转换进行模式匹配
sealed class PaymentMethod {
data class CreditCard(val number: String, val expiry: String) : PaymentMethod()
data class WeChatPay(val openId: String) : PaymentMethod()
data class BankTransfer(val account: String, val bank: String) : PaymentMethod()
}
fun processPayment(method: PaymentMethod): String = when (method) {
is PaymentMethod.CreditCard -> "信用卡: ${method.number.takeLast(4)}"
is PaymentMethod.WeChatPay -> "微信: ${method.openId.take(8)}"
is PaymentMethod.BankTransfer -> "银行: ${method.bank}"
}
// 带守卫条件的匹配(Kotlin 1.7+)
fun describe(expr: Expr): String = when {
expr is Expr.Const && expr.value == 0.0 -> "零"
expr is Expr.Const -> "常量 ${expr.value}"
expr is Expr.Sum -> "加法"
expr is Expr.Mul -> "乘法"
expr is Expr.Neg -> "取反"
else -> "未知"
}
常见场景
UI 状态建模
// 使用密封类建模屏幕状态
sealed class ScreenState<out T> {
object Idle : ScreenState<Nothing>()
object Loading : ScreenState<Nothing>()
data class Success<T>(val data: T) : ScreenState<T>()
data class Error(val message: String, val retry: (() -> Unit)? = null) : ScreenState<Nothing>()
}
// 在 ViewModel 中使用
class UserViewModel : ViewModel() {
private val _state = MutableStateFlow<ScreenState<User>>(ScreenState.Idle)
val state: StateFlow<ScreenState<User>> = _state.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_state.value = ScreenState.Loading
_state.value = try {
ScreenState.Success(repository.fetchUser(id))
} catch (e: Exception) {
ScreenState.Error(e.message ?: "加载失败") { loadUser(id) }
}
}
}
}
导航事件建模
// 使用密封类定义导航事件
sealed class NavigationEvent {
data class ToDetail(val userId: String) : NavigationEvent()
data class ToSettings(val tab: String) : NavigationEvent()
object Back : NavigationEvent()
data class DeepLink(val uri: String) : NavigationEvent()
}
// 处理导航
fun navigate(event: NavigationEvent) = when (event) {
is NavigationEvent.ToDetail -> navController.navigate("user/${event.userId}")
is NavigationEvent.ToSettings -> navController.navigate("settings/${event.tab}")
NavigationEvent.Back -> navController.popBackStack()
is NavigationEvent.DeepLink -> navController.navigate(Uri.parse(event.uri))
}
注意事项
- Kotlin 1.5 之前子类必须在同一文件中,1.5+ 允许同一包中不同文件
- 密封类构造器默认是 protected,可以改为 private 但不能是 public
- when 表达式如果不穷举所有分支,编译器会报错(除非添加 else)
- 密封接口不能有构造参数,但可以有抽象属性和方法
- 密封类配合泛型时注意型变,Result
是 Result 的子类型 - 在 Android 中推荐使用密封类替代枚举来表示 UI 状态
进阶用法
密封类与序列化
// 密封类配合 JSON 序列化(使用类鉴别器)
@Serializable
sealed class Message {
@Serializable
@SerialName("text")
data class Text(val content: String) : Message()
@Serializable
@SerialName("image")
data class Image(val url: String, val width: Int, val height: Int) : Message()
@Serializable
@SerialName("system")
data class System(val action: String) : Message()
}
// 序列化和反序列化
val json = Json { ignoreUnknownKeys = true }
val message: Message = json.decodeFromString(jsonString)
val serialized: String = json.encodeToString(message)
密封类与 StateFlow 结合
// 使用密封类 + StateFlow 构建单向数据流
sealed class Wish {
data class LoadItems(val category: String) : Wish()
data class DeleteItem(val id: String) : Wish()
object Refresh : Wish()
}
class ItemViewModel : ViewModel() {
private val _state = MutableStateFlow<ScreenState<List<Item>>>(ScreenState.Idle)
val state = _state.asStateFlow()
fun accept(wish: Wish) {
when (wish) {
is Wish.LoadItems -> loadItems(wish.category)
is Wish.DeleteItem -> deleteItem(wish.id)
Wish.Refresh -> refresh()
}
}
}