1. 概述

枚举(enum) 提供了一种类型安全、不易出错且自文档化的方式来处理常量。此外,由于枚举本质上也是类,它们可以拥有属性和方法。

在本文中,我们将探讨如何通过一个给定的属性值,查找对应的枚举实例。

2. 问题引入

我们来看一个简单的例子:

enum class Number(val value: Int) {
    ONE(1), TWO(2), THREE(3),
}

这是一个包含三个实例的枚举类 Number,每个实例都有一个 Int 类型的属性 value

现在的问题是:给定一个整数值 v,如何找到对应的枚举实例 e,使得 e.value == v

比如,当 v = 3 时,应返回 THREE;而当 v = 42 时,没有匹配项,我们统一返回 null

为了便于讲解,我们将为每种实现方式使用不同的类名,如 NumberV1NumberV2 等。

3. 方法一:遍历枚举实例

最直观的思路是:遍历所有枚举实例,找到匹配的属性值。

Kotlin 提供了 values() 方法来获取所有枚举实例。我们可以结合 firstOrNull() 方法来查找第一个匹配项:

enum class NumberV1(val value: Int) {
    ONE(1), TWO(2), THREE(3);

    companion object {
        infix fun from(value: Int): NumberV1? = NumberV1.values().firstOrNull { it.value == value }
    }
}

这里我们使用了 infix 函数,使得调用更接近自然语言:

val result = NumberV1 from 1

优点:实现简单,无需额外结构
缺点:时间复杂度为 O(n),每次调用都要遍历

测试代码如下:

val searchOne = NumberV1 from 1
assertEquals(NumberV1.ONE, searchOne)

val shouldBeNull = NumberV1 from 42
assertNull(shouldBeNull)

测试通过,说明方法有效。

4. 方法二:使用 Map 缓存枚举实例

方法一的问题在于每次查找都要遍历枚举实例。我们可以利用 Map 来缓存 value -> enum 的映射关系,从而将查找复杂度降到 O(1)。

4.1. 实现方式

enum class NumberV2(val value: Int) {
    ONE(1), TWO(2), THREE(3);

    companion object {
        private val map = NumberV2.values().associateBy { it.value }
        infix fun from(value: Int) = map[value]
    }
}

这里我们使用了 associateBy() 方法,将枚举实例数组转换为一个 Map,键为 value,值为对应的枚举实例。

调用方式与方法一相同:

val result = NumberV2 from 2

优点:查找效率高,O(1)
缺点:需要维护一个 Map,增加了内存开销

测试代码如下:

val searchTwo = NumberV2 from 2
assertEquals(NumberV2.TWO, searchTwo)

val shouldBeNull = NumberV2 from 42
assertNull(shouldBeNull)

测试通过。

4.2. 使用操作符重载

Kotlin 支持操作符重载。我们可以重载 get() 方法,让查找更像数组访问:

companion object {
    private val map = NumberV2.values().associateBy { it.value }
    operator fun get(value: Int) = map[value]
}

调用方式如下:

val result = NumberV2[3]

测试代码如下:

val searchThree = NumberV2[3]
assertEquals(NumberV2.THREE, searchThree)

优点:语法更简洁,可读性更强
⚠️ 注意:需谨慎使用操作符重载,避免语义不清晰

5. 方法三:封装为通用的 EnumFinder 类

如果多个枚举类都需要支持“通过属性查找”,我们可以将其封装为一个通用类。

abstract class EnumFinder<V, E>(private val valueMap: Map<V, E>) {
    infix fun from(value: V) = valueMap[value]
}

然后让枚举类的 companion object 继承该类,并传入对应的映射关系:

enum class NumberV3(val value: Int) {
    ONE(1), TWO(2), THREE(3);

    companion object : EnumFinder<Int, NumberV3>(NumberV3.values().associateBy { it.value })
}

调用方式如下:

val result = NumberV3 from 2

优点:可复用性强,结构清晰
缺点:需要为每个枚举类添加继承关系

测试代码如下:

val searchTwo = NumberV3 from 2
assertEquals(NumberV3.TWO, searchTwo)

测试通过。

6. 方法四:通用的 findBy 函数(无需修改枚举)

前面的方法都需要在枚举类中添加查找逻辑。如果项目中有很多枚举类都需要支持查找功能,这种方法会带来重复代码。

我们可以通过泛型和方法引用,实现一个通用的 findBy 函数,无需修改枚举类本身

6.1. 实现方式

infix inline fun <reified E : Enum<E>, V> ((E) -> V).findBy(value: V): E? {
    return enumValues<E>().firstOrNull { this(it) == value }
}

这个函数是一个扩展函数,接受一个属性访问函数(如 NumberV4::value)和一个目标值,返回匹配的枚举实例。

调用方式如下:

val result = NumberV4::value findBy 2

6.2. 测试示例

假设我们有两个枚举类:

enum class NumberV4(val value: Int) {
    ONE(1), TWO(2), THREE(3),
}

enum class OS(val input: String) {
    Linux("linux"), MacOs("mac"),
}

测试代码如下:

val searchOne = NumberV4::value findBy 1
assertEquals(NumberV4.ONE, searchOne)

val linux = OS::input findBy "linux"
assertEquals(OS.Linux, linux)

val windows = OS::input findBy "windows"
assertNull(windows)

优点:完全解耦,无需修改枚举类
缺点:实现复杂,需要理解高阶函数和 reified 类型参数
⚠️ 注意:该函数依赖 Kotlin 的 enumValues() 方法,仅适用于 JVM 平台

7. 总结

本文介绍了四种在 Kotlin 中通过属性值查找枚举实例的方法:

方法 实现方式 优点 缺点
方法一 遍历枚举实例 简单直接 性能差(O(n))
方法二 使用 Map 缓存 查找快(O(1)) 增加内存开销
方法三 封装为 EnumFinder 类 可复用性强 每个类需继承
方法四 泛型 findBy 函数 无需修改枚举 实现复杂

根据实际场景选择合适的方法:

  • 枚举数量少、查找频率低 → 方法一
  • 枚举数量多、查找频繁 → 方法二
  • 多个枚举类需复用逻辑 → 方法三
  • 不想修改枚举类 → 方法四

完整代码示例可在 GitHub 获取。


原始标题:Finding Enum by the Value in Kotlin