数据类、密封类与枚举

Kotlin 提供了多种专门表达数据和有限状态的类型:data classsealed classsealed interfaceenum class。它们能减少样板代码,也能让状态建模更安全。

数据类

data class User(val id: Long, val name: String)

编译器会基于主构造函数中的属性生成:

  • equals()
  • hashCode()
  • toString()
  • copy()
  • componentN() 解构函数

示例:

val user = User(1, "Ada")
val renamed = user.copy(name = "Grace")

println(user)      // User(id=1, name=Ada)
println(renamed)   // User(id=1, name=Grace)

Java 对比:Java 16+ 的 record 也能减少数据载体样板代码,但 Kotlin 数据类支持 copy()、默认参数、解构等 Kotlin 风格能力。二者互操作时要分别理解各自生成的 API。

数据类的边界

数据类适合表达值对象或 DTO,不适合所有类:

  • 不要把复杂业务行为都塞进数据类。
  • copy() 是浅拷贝,不会递归复制内部对象。
  • 主构造函数以外声明的属性不参与生成的 equals()hashCode() 等方法。

示例:

data class Team(val name: String, val members: MutableList<String>)

val a = Team("Core", mutableListOf("Ada"))
val b = a.copy()

b.members += "Grace"
println(a.members) // [Ada, Grace]

copy() 后两个对象仍共享同一个可变列表引用。

枚举类

枚举适合表示一组固定常量:

enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

枚举可以有属性和方法:

enum class HttpStatus(val code: Int) {
    OK(200),
    NOT_FOUND(404),
    INTERNAL_ERROR(500);

    fun isError(): Boolean = code >= 400
}

Java 枚举也支持属性和方法,Kotlin 枚举的差异主要体现在更自然地配合 when 表达式和 Kotlin 属性语法。

密封类与密封接口

密封类型用于表达“有限但每种情况可以携带不同数据”的层级:

sealed interface UiState

data object Loading : UiState
data class Success(val data: String) : UiState
data class Failure(val message: String) : UiState

处理状态:

fun render(state: UiState): String =
    when (state) {
        Loading -> "加载中"
        is Success -> "数据:${state.data}"
        is Failure -> "错误:${state.message}"
    }

这里不需要 else,因为编译器知道 UiState 的所有直接实现。

密封类 vs 枚举

需求 推荐
每个值只是固定常量 enum class
每个分支需要携带不同数据 sealed class / sealed interface
需要单例状态 data object 或普通 object
需要每个常量有相同结构属性 enum class
需要层级扩展但受控 密封类型

示例:支付方式用密封类通常比枚举更合适:

sealed interface PaymentMethod

data class CreditCard(val number: String, val cvc: String) : PaymentMethod
data class Paypal(val email: String) : PaymentMethod
data object Cash : PaymentMethod

不同支付方式需要不同字段,枚举很难自然表达。

Java sealed 对比

Java 15 引入 sealed class,需要配合 permits 声明允许的子类。Kotlin 的密封类更早出现,语法更贴合 when 穷尽检查。

Java 思路:

public sealed interface Result permits Success, Failure {}

Kotlin 思路:

sealed interface Result
data class Success(val value: String) : Result
data class Failure(val reason: String) : Result

Kotlin 的直接子类通常需要在同一包和同一模块内,具体限制还会受到多平台 source set 的影响。

实践建议

  • DTO 和值对象优先考虑 data class
  • 表示有限状态时,优先考虑密封类型,而不是字符串常量。
  • 枚举适合“固定命名值”,不适合每个分支结构差异很大的场景。
  • copy() 是浅拷贝,包含可变集合时要特别小心。
  • when 处理密封类型时尽量不写无意义 else,保留编译器穷尽检查。

参考

  • 官方数据类文档:https://kotlinlang.org/docs/data-classes.html
  • 官方密封类文档:https://kotlinlang.org/docs/sealed-classes.html
  • 官方枚举文档:https://kotlinlang.org/docs/enum-classes.html