跳到主要内容

Kotlin 枚举类实战:用 enum class 告别魔法数字和散落的常量

· 阅读需 6 分钟
重庆 Kotlin User Group

你写过这样的代码吗?

// 订单状态:0=待支付,1=已支付,2=已发货,3=已完成,4=已取消
fun handleOrder(status: Int) {
if (status == 0) {
println("等待支付...")
} else if (status == 1) {
println("已支付,准备发货")
} else if (status == 2) {
println("已发货")
} else if (status == 3) {
println("订单完成")
} else if (status == 4) {
println("已取消")
}
// 如果传进来一个 5 呢?静默忽略了...
}

这段代码有三个问题:第一,0, 1, 2, 3, 4 是"魔法数字"——读代码的人根本不知道它们代表什么;第二,if-else 链又臭又长;第三,如果有人传了个 5,代码会静默忽略,不报任何错。

Kotlin 的 enum class(枚举类) 可以一次性解决这三个问题,而且远比你想象的强大。

第一层:最基础的枚举​

enum class OrderStatus {
PENDING, // 待支付
PAID, // 已支付
SHIPPED, // 已发货
COMPLETED, // 已完成
CANCELLED // 已取消
}

现在,状态不再是神秘的数字,而是有名字的常量。用 == 比较即可,不需要 equals():

val status = OrderStatus.PAID
if (status == OrderStatus.PAID) {
println("已支付")
}

光这一点,就已经比 if (status == 1) 强太多了。但枚举的能力远不止于此。

第二层:给枚举加上属性和方法​

Kotlin 的枚举不只是"命名常量"——它可以有构造函数、属性和方法,就像一个普通的类。

enum class OrderStatus(val label: String, val code: Int) {
PENDING("待支付", 0),
PAID("已支付", 1),
SHIPPED("已发货", 2),
COMPLETED("已完成", 3),
CANCELLED("已取消", 4); // 注意:最后一个常量后面是分号

// 枚举的方法
fun isTerminal(): Boolean = this == COMPLETED || this == CANCELLED

fun canTransitionTo(target: OrderStatus): Boolean {
val allowed = mapOf(
PENDING to listOf(PAID, CANCELLED),
PAID to listOf(SHIPPED, CANCELLED),
SHIPPED to listOf(COMPLETED),
COMPLETED to emptyList(),
CANCELLED to emptyList()
)
return target in (allowed[this] ?: emptyList())
}
}

// 使用
val status = OrderStatus.PENDING
println(status.label) // 待支付
println(status.isTerminal()) // false
println(status.canTransitionTo(OrderStatus.SHIPPED)) // false(不能跳过支付直接发货)

这段代码做了一件很漂亮的事:把状态转移规则封装到了枚举本身。不需要在外面写一堆 if-else,枚举自己就知道"谁能转到谁"。

第三层:枚举 + when = 穷举检查​

枚举和 when 表达式是天生一对。当 when 覆盖了所有枚举值时,编译器会做 穷举检查——不需要写 else 分支:

fun describeStatus(status: OrderStatus): String = when (status) {
OrderStatus.PENDING -> "等待买家支付"
OrderStatus.PAID -> "已支付,通知商家发货"
OrderStatus.SHIPPED -> "包裹在路上了"
OrderStatus.COMPLETED -> "交易成功!"
OrderStatus.CANCELLED -> "订单已取消"
// 不需要 else!编译器知道所有枚举值都覆盖了
}

穷举检查意味着:如果你以后加了一个新状态(比如 REFUNDING),但忘了在 when 里处理它,编译器会直接报错,而不是等到运行时才出 Bug。

enum class OrderStatus {
PENDING, PAID, SHIPPED, COMPLETED, CANCELLED, REFUNDING // 新增!
}

// 之前的 when 没有处理 REFUNDING → 编译报错!
fun describeStatus(status: OrderStatus): String = when (status) {
OrderStatus.PENDING -> "等待买家支付"
// ... 其他分支
// 编译器:你漏了 REFUNDING!
}

这就是枚举的威力——让编译器帮你检查逻辑完整性。

第四层:抽象方法——每个枚举常量有自己的行为​

这是 90% 的 Kotlin 开发者都不知道的"隐藏技能":枚举可以定义抽象方法,每个枚举常量分别实现它。

enum class Calculator {
ADD {
override fun apply(a: Int, b: Int) = a + b
},
SUBTRACT {
override fun apply(a: Int, b: Int) = a - b
},
MULTIPLY {
override fun apply(a: Int, b: Int) = a * b
},
DIVIDE {
override fun apply(a: Int, b: Int): Int {
require(b != 0) { "除数不能为零!" }
return a / b
}
};

abstract fun apply(a: Int, b: Int): Int
}

// 使用
println(Calculator.ADD.apply(10, 3)) // 13
println(Calculator.MULTIPLY.apply(10, 3)) // 30
println(Calculator.DIVIDE.apply(10, 3)) // 3

// 甚至可以当参数传
fun compute(op: Calculator, a: Int, b: Int) = op.apply(a, b)
println(compute(Calculator.SUBTRACT, 10, 3)) // 7

每个枚举常量都是一个独立的对象,拥有自己的行为实现。这比 switch-case 优雅太多了——你不需要在外部用 when 判断"如果是 ADD 就加,如果是 SUBTRACT 就减",行为直接内聚在枚举里。

遍历枚举:entries 属性​

Kotlin 1.9 起,推荐使用 entries 属性来遍历所有枚举常量(替代旧的 values() 方法):

// 遍历所有状态
for (status in OrderStatus.entries) {
println("${status.name}: ${status.label}")
}

// 输出:
// PENDING: 待支付
// PAID: 已支付
// SHIPPED: 已发货
// COMPLETED: 已完成
// CANCELLED: 已取消

entries 返回的是一个不可变的 EnumEntries 列表,性能比 values() 更好(不会每次调用都创建新数组)。

通过名称获取枚举常量:

val status = OrderStatus.valueOf("PAID") // OrderStatus.PAID
// 如果名称不存在,会抛出 IllegalArgumentException

实战:用枚举建模游戏状态​

把枚举的各种能力组合起来,做一个小游戏的角色状态系统:

enum class HeroState(val emoji: String) {
IDLE("🧍") {
override fun takeDamage(hero: Hero): HeroState {
return if (hero.hp > 0) HIT else DEAD
}
},
HIT("💥") {
override fun takeDamage(hero: Hero): HeroState {
return if (hero.hp > 0) HIT else DEAD
}
},
DEAD("💀") {
override fun takeDamage(hero: Hero): HeroState = DEAD
},
HEALING("💚") {
override fun takeDamage(hero: Hero): HeroState = HIT // 治疗中被打断
};

abstract fun takeDamage(hero: Hero): HeroState

fun describe(hero: Hero) = "$emoji ${hero.name} 处于 $name 状态 (HP: ${hero.hp})"
}

data class Hero(val name: String, var hp: Int) {
var state: HeroState = HeroState.IDLE

fun takeDamage(damage: Int) {
hp = (hp - damage).coerceAtLeast(0)
state = state.takeDamage(this)
println(state.describe(this))
}
}

fun main() {
val hero = Hero("勇者", hp = 100)
hero.takeDamage(30) // 💥 勇者 处于 HIT 状态 (HP: 70)
hero.takeDamage(70) // 💀 勇者 处于 DEAD 状态 (HP: 0)
hero.takeDamage(10) // 💀 勇者 处于 DEAD 状态 (HP: 0) — 已经死了,不会再变
}

每个状态有自己的受击行为,IDLE 和 HIT 被打会扣血,DEAD 不会再死一次,HEALING 被打断会回到 HIT——所有逻辑都内聚在枚举里,清晰又好维护。

小结​

层级能力适用场景
基础枚举命名常量,替代魔法数字状态、类型、方向等固定选项
带属性每个常量绑定附加数据HTTP 状态码、配置项
带方法封装行为逻辑状态转移规则、校验逻辑
抽象方法每个常量有不同实现策略模式、多态行为
+ when穷举检查确保所有情况都被处理

一句话总结:如果你的类只有一组固定的实例,就用 enum class。 它比魔法数字安全,比 if-else 链清晰,比你想象的更强大。


今日小挑战:定义一个 enum class DayOfWeek(周一到周日),给每个枚举常量加上一个 isWeekend: Boolean 属性。然后写一个函数 fun schedule(day: DayOfWeek): String,用 when 表达式为工作日返回"上班",为周末返回"休息"。试试看,编译器会不会帮你检查所有天都被覆盖了?