密封类基础:什么是密封类、定义语法与枚举类的区别

好,咱们直接进入正题。

密封类(Sealed Class)这个概念,我第一次接触时觉得它挺「拧巴」的——它既像枚举,又像抽象类。后来在项目里踩过几次坑,才真正理解它的设计意图。说白了,密封类就是 Kotlin 给「有限子类型」问题准备的一把手术刀。

什么是密封类

先问个问题:你有没有遇到过这种情况——一个父类,你知道它只有固定的几种子类型,但编译器不知道?

比如网络请求的结果,要么是成功,要么是失败,要么是加载中。这三种情况,你心里门儿清。但如果你用普通抽象类,别人随便写个子类继承过去,你的 when 表达式就得加 else 分支。这就是痛点。

密封类就是来解决这个的。它告诉编译器:「嘿,我这个类的子类型就这么多,别的不许有。」

核心定义:密封类是一种受限的类层次结构。它的所有直接子类必须在同一个文件内声明,并且是已知的、有限的。

我个人习惯把密封类理解为「带状态的枚举」。枚举是固定实例,密封类是固定类型。你想想看,枚举的每个值都是同一个类的实例,而密封类的每个子类可以有自己的状态和行为。这个区别很关键。

密封类的定义与语法

语法其实很简单,用 sealed 关键字修饰类即可。来看个例子:

sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val message: String, val code: Int) : Result()
    object Loading : Result()
}

嗯,这里要注意几个细节:

  • 密封类本身是抽象的,不能直接实例化
  • 子类可以是 data class、object 或者普通 class
  • 所有子类必须在同一个文件里(Kotlin 1.1 之前要求更严格,现在放宽了,但建议还是放一起)

我在项目中遇到过这样一个场景:需要表示用户界面的不同状态。用密封类简直不要太舒服:

sealed class UiState {
    data class Display(val items: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
    object Loading : UiState()
    object Empty : UiState()
}

配合 when 表达式使用时,编译器会帮你检查是否覆盖了所有分支:

fun render(state: UiState) {
    when (state) {
        is UiState.Display -> showList(state.items)
        is UiState.Error -> showError(state.message)
        UiState.Loading -> showSpinner()
        UiState.Empty -> showEmptyHint()
    }
    // 不需要 else 分支!
}

我曾经因为漏掉一个分支导致线上 bug,自那以后,凡是有限状态集我都用密封类。编译器帮我兜底,心里踏实。

密封类与枚举类的区别

很多初学者会问:「枚举也能表示状态啊,为什么还要用密封类?」

好问题。咱们来对比一下:

对比维度 枚举类 密封类
本质 一组固定的常量实例 一组固定的子类型
每个值/子类的状态 所有实例结构相同 每个子类可以有不同的属性
实例数量 每个枚举值只有一个实例 每个子类可以有多个实例
数据携带 通过构造函数参数,但所有值参数相同 每个子类可以定义不同的数据字段
适用场景 简单的状态标识(如颜色、方向) 需要携带不同数据的复杂状态

说白了,枚举适合「是什么」的问题,密封类适合「有什么」的问题。

举个例子:

// 枚举:只能表示状态名称
enum class Status { SUCCESS, ERROR, LOADING }

// 密封类:每个状态可以携带不同数据
sealed class Status {
    data class Success(val data: String) : Status()
    data class Error(val message: String, val code: Int) : Status()
    object Loading : Status()
}

你看,枚举的 SUCCESS 就是一个光杆司令,而密封类的 Success 可以带着 data 到处跑。这就是本质区别。

我的建议:如果只是标记状态,不需要额外数据,用枚举就够了。一旦需要每个状态携带不同的数据,或者状态之间有行为差异,果断上密封类。

避坑指南

最后分享几个我踩过的坑:

我曾经在密封类里用了内部类,结果导致子类无法在外部访问。记住:密封类的子类必须是顶级类,或者嵌套在密封类内部,但不能是内部类(没有 inner 关键字)。

还有一个容易忽略的点:密封类的子类不能是局部类(定义在函数内部的类)。这个限制其实很合理——局部类的作用域太窄,违背了密封类「有限且已知」的设计初衷。

嗯,关于密封类的基础知识就聊到这里。记住三个关键词:有限子类型、编译器检查、状态携带。下一节咱们会深入密封类的实际应用场景,看看它在状态管理设计模式中到底能发挥多大威力。