密封接口与密封类: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)
        }
    }
}
我的小建议:如果子类之间没有「is-a」的层级关系,只是平级的有限变体,优先考虑 sealed interface。它更轻量,也更灵活。

模式匹配: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
        }
    }
}
我曾经踩过的坑:密封类的子类如果定义在同一个文件的不同位置,Kotlin 编译器会报错。所有子类必须和密封类在同一个文件内,或者嵌套在密封类内部。这个限制在 Kotlin 1.5 之后有所放宽(允许不同文件但同一编译单元),但我建议还是放在同一个文件里,清晰明了。

密封接口的「多继承」优势

密封接口有一个密封类做不到的事:一个类可以实现多个密封接口。这在设计复杂状态机时非常有用。

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")
    }
}

这种设计模式,说白了就是「按能力组合」。每个密封接口代表一种能力,实现类按需选择。比用抽象类硬塞一堆抽象方法优雅多了。

知识体系结构图

密封接口与密封类知识体系 sealed class / interface sealed class sealed interface 共享状态/构造函数 单继承限制 多实现能力 行为契约定义 when 穷尽匹配 + 模式匹配 状态机 网络请求封装 UI 事件处理

实战:用密封接口构建 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 事件处理时,不妨试试看。

公众号:蓝海资料掘金营,微信deep3321