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


原始标题:Sealed Class vs Enum in Kotlin