密封接口与密封类:sealed interface、sealed class 的进阶用法、模式匹配
说实话,密封类(sealed class)和密封接口(sealed interface)是我在 Kotlin 项目中最喜欢用的特性之一。为什么?因为它们让代码变得「可预测」。你想想看,当你看到一个 when 表达式不需要写 else 分支时,那种安全感——嗯,这就是密封设计带来的好处。
我记得刚接触 Kotlin 时,还在用枚举 + 抽象类的方式模拟有限状态。后来发现密封类才是正解。今天我们就来聊聊它们的进阶用法,以及模式匹配怎么玩出花来。
密封类 vs 密封接口:什么时候选哪个?
先理清一个基本问题:sealed class 和 sealed interface 到底有什么区别?
我个人习惯这样判断:
- 需要共享状态或构造函数参数 → 用 sealed class
- 只需要定义行为契约,子类各自实现 → 用 sealed interface
- 子类需要继承其他类 → 必须用 sealed interface(因为 Kotlin 单继承)
来看个实际例子。我在做一个支付系统时,定义了支付结果:
// 用 sealed class 因为需要携带数据
sealed class PaymentResult {
data class Success(val transactionId: String, val amount: Double) : PaymentResult()
data class Failure(val errorCode: Int, val message: String) : PaymentResult()
object Pending : PaymentResult()
}
// 用 sealed interface 因为只定义行为
sealed interface PaymentAction {
fun validate(): Boolean
fun execute(): PaymentResult
class CreditCard(val number: String, val cvv: String) : PaymentAction {
override fun validate(): Boolean = number.length == 16
override fun execute(): PaymentResult {
// 处理逻辑
return PaymentResult.Success("txn_001", 99.9)
}
}
class WeChatPay(val openId: String) : PaymentAction {
override fun validate(): Boolean = openId.isNotBlank()
override fun execute(): PaymentResult {
return PaymentResult.Success("txn_002", 199.0)
}
}
}
模式匹配:when 表达式的进阶玩法
密封类最大的价值,就是配合 when 表达式做穷尽匹配。但很多人只用了最基础的形式。其实可以玩得更高级。
1. 带条件的模式匹配
fun processPayment(result: PaymentResult): String {
return when (result) {
is PaymentResult.Success -> {
// 可以加条件分支
if (result.amount > 1000) "大额支付成功: ${result.transactionId}"
else "小额支付成功: ${result.transactionId}"
}
is PaymentResult.Failure -> {
when {
result.errorCode == 401 -> "认证失败,请重新登录"
result.errorCode in 500..599 -> "服务端异常,请稍后重试"
else -> "支付失败: ${result.message}"
}
}
is PaymentResult.Pending -> "支付处理中,请等待"
}
}
你看,when 里面还可以嵌套 when。这在处理复杂业务逻辑时特别有用。我在项目中遇到过一种场景:同一个错误码在不同上下文下含义不同,用这种嵌套匹配可以清晰表达。
2. 密封类 + 泛型:类型安全的构建器
sealed class ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Error(val code: Int, val message: String) : ApiResponse<Nothing>()
object Loading : ApiResponse<Nothing>()
}
fun <T> handleResponse(response: ApiResponse<T>): T? {
return when (response) {
is ApiResponse.Success -> response.data
is ApiResponse.Error -> {
println("错误 ${response.code}: ${response.message}")
null
}
is ApiResponse.Loading -> {
println("加载中...")
null
}
}
}
密封接口的「多继承」优势
密封接口有一个密封类做不到的事:一个类可以实现多个密封接口。这在设计复杂状态机时非常有用。
sealed interface Editable {
fun edit(content: String)
}
sealed interface Deletable {
fun delete()
}
sealed interface Shareable {
fun share(to: String)
}
// 一个类可以实现多个密封接口
class Document(val id: String, var content: String) : Editable, Deletable, Shareable {
override fun edit(content: String) {
this.content = content
println("文档 $id 已更新")
}
override fun delete() {
println("文档 $id 已删除")
}
override fun share(to: String) {
println("文档 $id 已分享给 $to")
}
}
// 只读文档只能分享,不能编辑或删除
class ReadOnlyDocument(val id: String, val content: String) : Shareable {
override fun share(to: String) {
println("只读文档 $id 已分享给 $to")
}
}
这种设计模式,说白了就是「按能力组合」。每个密封接口代表一种能力,实现类按需选择。比用抽象类硬塞一堆抽象方法优雅多了。
知识体系结构图
实战:用密封接口构建 UI 事件总线
最后分享一个我在实际项目中的用法。我们团队之前做 IM 应用,消息类型特别多。用密封接口来定义 UI 事件,配合模式匹配,代码清晰到不行。
sealed interface ChatEvent {
data class TextMessage(val content: String, val timestamp: Long) : ChatEvent
data class ImageMessage(val url: String, val width: Int, val height: Int) : ChatEvent
data class VoiceMessage(val duration: Int, val filePath: String) : ChatEvent
object TypingIndicator : ChatEvent
data class SystemNotification(val type: String, val message: String) : ChatEvent
}
fun renderChatEvent(event: ChatEvent): String {
return when (event) {
is ChatEvent.TextMessage -> "文本: ${event.content}"
is ChatEvent.ImageMessage -> "图片: ${event.url} (${event.width}x${event.height})"
is ChatEvent.VoiceMessage -> "语音: ${event.duration}秒"
is ChatEvent.TypingIndicator -> "对方正在输入..."
is ChatEvent.SystemNotification -> "[系统] ${event.message}"
}
}
你看,没有 else 分支。将来如果新增一种消息类型,编译器会直接报错,提醒你所有 when 表达式都要处理新类型。这种「编译时安全」的感觉,用过的都说好。
- 需要共享状态用 sealed class,需要多实现用 sealed interface
- when 表达式配合密封类可以做到穷尽匹配,无需 else
- 密封接口支持一个类实现多个,适合按能力组合的设计
- 模式匹配可以嵌套条件,处理复杂业务逻辑
- 所有子类必须在同一文件内(或同一编译单元)
嗯,密封类这东西,说白了就是 Kotlin 给「有限类型集合」这个场景量身定做的解决方案。用好了,代码的可维护性会上一个台阶。下次写状态机、网络请求封装、UI 事件处理时,不妨试试看。