1. 简介
在编程中,我们常常需要对类型常量进行区分。早期的做法是通过定义静态变量来实现,这种方式虽然不算错,但显然有更好的替代方案。Kotlin 提供了多种机制来处理这类需求。
本文将重点分析两种最常用的类型:Sealed Class(密封类) 和 Enum Class(枚举类),它们都适用于表示有限且封闭的类型集合,但在使用场景和灵活性上有显著差异。
我们会从声明方式、函数定义、实际用法以及适用场景等方面进行深入对比,帮助你在实际项目中做出更合理的选择,避免踩坑 ❌。
2. Sealed Class(密封类)
密封类用于定义一个封闭的类继承体系,其所有子类必须在编译期就明确声明,并且只能在同一个文件中定义。
✅ 本质上是一个“受限的继承”机制,非常适合用来建模有限状态或结果类型(如 Result<T, E>)。
2.1 声明方式
我们先用 sealed class
来表示不同的操作系统:
sealed class OsSealed {
object Linux : OsSealed()
object Windows : OsSealed()
object Mac : OsSealed()
}
如果需要携带额外信息,比如操作系统所属公司,可以添加构造参数:
sealed class OsSealed(val company: String) {
object Linux : OsSealed("Open-Source")
object Windows : OsSealed("Microsoft")
object Mac : OsSealed("Apple")
}
⚠️ 注意:
- 密封类不能被外部文件继承(即子类必须在同一文件内)。
- 子类可以是
object
(单例)、普通类或data class
,非常灵活。
2.2 使用方式
最常见的使用方式是结合 when
表达式。由于密封类的子集已知,编译器能检查是否覆盖所有分支,无需 else
分支(除非有 Unknown
这种兜底项):
when (osSealed) {
OsSealed.Linux -> println("${osSealed.company} - Linux Operating System")
OsSealed.Mac -> println("${osSealed.company} - Mac Operating System")
OsSealed.Windows -> println("${osSealed.company} - Windows Operating System")
}
2.3 函数定义
密封类的一大优势是允许不同子类拥有各自独立的方法签名。
例如,我们可以为每个操作系统定义特定行为:
sealed class OsSealed(val releaseYear: Int = 0, val company: String = "") {
constructor(company: String) : this(0, company)
object Linux : OsSealed("Open-Source") {
fun getText(value: Int): String {
return "Linux by $company - value=$value"
}
}
object Windows : OsSealed("Microsoft") {
fun getNumber(value: String): Int {
return value.length
}
}
object Mac : OsSealed(2001, "Apple") {
fun doSomething(): String {
val s = "Mac by $company - released at $releaseYear"
println(s)
return s
}
}
object Unknown : OsSealed()
fun getTextParent(): String {
return "Called from parent sealed class"
}
}
调用时需要注意:
- 父类方法可直接调用 ✅
- 子类特有方法需通过类型判断 + 安全调用,推荐使用
is
操作符避免显式强转 ❌
assertEquals("Called from parent sealed class", osSealed.getTextParent())
when (osSealed) {
is OsSealed.Linux -> assertEquals("Linux by Open-Source - value=1", osSealed.getText(1))
is OsSealed.Mac -> assertEquals("Mac by Apple - released at 2001", osSealed.doSomething())
is OsSealed.Windows -> assertEquals(5, osSealed.getNumber("Text!"))
else -> assertTrue(osSealed is OsSealed.Unknown)
}
✅ 使用 is
配合 when
可自动智能转换(smart cast),代码更简洁安全。
3. Enum Class(枚举类)
枚举类用于定义一组命名的常量值,每个枚举实例都是唯一的单例对象。
适合表示固定集合的状态码、类型标识等场景。
3.1 声明方式
最基本的枚举定义如下:
enum class OsEnum {
Linux,
Windows,
Mac
}
也可以带参数,类似 Java 枚举:
enum class OsEnum(val company: String) {
Linux("Open-Source"),
Windows("Microsoft"),
Mac("Apple")
}
3.2 使用方式
同样支持 when
表达式,语法几乎一致:
when (osEnum) {
OsEnum.Linux -> println("${osEnum.company} - Linux Operating System")
OsEnum.Mac -> println("${osEnum.company} - Mac Operating System")
OsEnum.Windows -> println("${osEnum.company} - Windows Operating System")
}
3.3 函数定义
枚举类支持两种函数定义方式:
- 在父类中定义通用方法(所有枚举共享)
- 为每个枚举项重写抽象方法(类似 Java 的 enum 抽象方法)
示例:
enum class OsEnum(val releaseYear: Int = 0, val company: String = "") {
Linux(0, "Open-Source") {
override fun getText(value: Int): String {
return "Linux by $company - value=$value"
}
},
Windows(0, "Microsoft") {
override fun getText(value: Int): String {
return "Windows by $company - value=$value"
}
},
Mac(2001, "Apple") {
override fun getText(value: Int): String {
return "Mac by $company - released at $releaseYear"
}
},
Unknown {
override fun getText(value: Int): String {
return ""
}
};
abstract fun getText(value: Int): String
fun getTextParent(): String {
return "Called from parent enum class"
}
}
调用方式:
assertEquals("Called from parent enum class", osEnum.getTextParent())
when (osEnum) {
OsEnum.Linux -> assertEquals("Linux by Open-Source - value=1", osEnum.getText(1))
OsEnum.Windows -> assertEquals("Windows by Microsoft - value=2", osEnum.getText(2))
OsEnum.Mac -> assertEquals("Mac by Apple - released at 2001", osEnum.getText(3))
else -> assertTrue(osEnum == OsEnum.Unknown)
}
⚠️ 缺点也很明显:
- 所有枚举项都必须实现
abstract
方法,哪怕逻辑为空(如Unknown
) - 构造参数必须统一,无法差异化设计
4. Sealed Class vs Enum:核心区别
特性 | Enum Class | Sealed Class |
---|---|---|
实例数量 | 固定,每个常量只有一个实例 | 子类可创建多个实例(尤其是非 object 类型) |
继承限制 | 所有项必须在枚举内部定义 | 子类可在同一文件内任意定义(包括 class , object , data class ) |
方法灵活性 | 所有项共享相同方法签名;若用抽象方法,则必须全部实现 | 每个子类可定义独有的方法、属性、构造器 |
数据承载能力 | 参数结构统一,难以差异化 | 支持复杂数据结构,各子类可不同 |
是否支持泛型 | 否 | 是(可用于构建 Result、Either 等类型) |
内存占用 | 极小,单例模式 | 相对较高(取决于子类实现) |
📌 总结一句话:
Enum 是“值”的集合,Sealed Class 是“类型”的集合。
5. 使用场景建议
选择哪个?关键看你的业务模型是否需要“差异化行为”和“灵活数据结构”。
推荐使用 Enum 的场景 ✅
- 表示一组固定常量,如 HTTP 状态码、订单状态(PENDING, PAID, CANCELLED)
- 不需要附加复杂逻辑或差异化数据
- 需要序列化、比较、遍历等原生支持(
values()
、valueOf()
)
enum class OrderStatus {
PENDING, PAID, SHIPPED, CANCELLED
}
推荐使用 Sealed Class 的场景 ✅
- 表示不同类型的消息、事件、响应结果
- 各类型携带的数据结构不同
- 需要在
when
中做智能转换并调用特定方法
典型例子:网络请求结果封装
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val code: Int) : Result<Nothing>()
object Loading : Result<Nothing>()
}
再比如 UI 事件分发、状态机建模等,都非常适合用密封类。
6. 结论
对比维度 | Enum | Sealed Class |
---|---|---|
简洁性 | ✅ 更简单直观 | ❌ 略复杂 |
灵活性 | ❌ 受限 | ✅ 极高 |
扩展性 | ❌ 固定实例 | ✅ 可扩展子类 |
推荐用途 | 固定常量集合 | 多态类型建模 |
🔹 如果只是简单的状态标识 → 用 Enum
🔹 如果涉及不同类型+不同数据+不同行为 → 用 Sealed Class
两者不是互斥关系,而是互补工具。理解它们的本质差异,才能在架构设计时做出更优雅的选择。
示例代码已整理至 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-lang-oop-2