在 Kotlin 使用反向查找的有效枚举?

我试图找到一个最好的方法来做一个“反向查找”在 Kotlin 枚举。有效 Java 给我的启示之一是,在枚举中引入一个静态映射来处理反向查找。通过一个简单的 enum 将其移植到 Kotlin,我得到了如下代码:

enum class Type(val value: Int) {
A(1),
B(2),
C(3);


companion object {
val map: MutableMap<Int, Type> = HashMap()


init {
for (i in Type.values()) {
map[i.value] = i
}
}


fun fromInt(type: Int?): Type? {
return map[type]
}
}
}

我的问题是,这是最好的办法,还是有更好的办法?如果我有几个遵循类似模式的枚举,该怎么办?在 Kotlin 有没有办法让这些代码在 enum 中更加可重用?

65787 次浏览

首先,fromInt()的参数应该是 Int,而不是 Int?。尝试使用 null 获取 Type显然会导致 null,而调用者甚至不应该尝试这样做。Map也没有理由是可变的。守则可简化为:

companion object {
private val map = Type.values().associateBy(Type::value)
fun fromInt(type: Int) = map[type]
}

这段代码太短了,坦率地说,我不确定是否值得尝试找到一个可重用的解决方案。

我发现自己多次通过自定义、手工编码、值进行反向查找,并提出了以下方法。

enum实现一个共享接口:

interface Codified<out T : Serializable> {
val code: T
}


enum class Alphabet(val value: Int) : Codified<Int> {
A(1),
B(2),
C(3);


override val code = value
}

此接口 (不管这名字多么奇怪)将某个值标记为显式代码:

val a = Alphabet::class.decode(1) //Alphabet.A
val d = Alphabet::class.tryDecode(4) //null

这可以很容易地实现与以下代码:

interface Codified<out T : Serializable> {
val code: T


object Enums {
private val enumCodesByClass = ConcurrentHashMap<Class<*>, Map<Serializable, Enum<*>>>()


inline fun <reified T, TCode : Serializable> decode(code: TCode): T where T : Codified<TCode>, T : Enum<*> {
return decode(T::class.java, code)
}


fun <T, TCode : Serializable> decode(enumClass: Class<T>, code: TCode): T where T : Codified<TCode> {
return tryDecode(enumClass, code) ?: throw IllegalArgumentException("No $enumClass value with code == $code")
}


inline fun <reified T, TCode : Serializable> tryDecode(code: TCode): T? where T : Codified<TCode> {
return tryDecode(T::class.java, code)
}


@Suppress("UNCHECKED_CAST")
fun <T, TCode : Serializable> tryDecode(enumClass: Class<T>, code: TCode): T? where T : Codified<TCode> {
val valuesForEnumClass = enumCodesByClass.getOrPut(enumClass as Class<Enum<*>>, {
enumClass.enumConstants.associateBy { (it as T).code }
})


return valuesForEnumClass[code] as T?
}
}
}


fun <T, TCode> KClass<T>.decode(code: TCode): T
where T : Codified<TCode>, T : Enum<T>, TCode : Serializable
= Codified.Enums.decode(java, code)


fun <T, TCode> KClass<T>.tryDecode(code: TCode): T?
where T : Codified<TCode>, T : Enum<T>, TCode : Serializable
= Codified.Enums.tryDecode(java, code)

在这种情况下没有多大意义,但是这里有一个@JBNized 解决方案的“逻辑提取”:

open class EnumCompanion<T, V>(private val valueMap: Map<T, V>) {
fun fromInt(type: T) = valueMap[type]
}


enum class TT(val x: Int) {
A(10),
B(20),
C(30);


companion object : EnumCompanion<Int, TT>(TT.values().associateBy(TT::x))
}


//sorry I had to rename things for sanity

一般来说,伴随对象可以被重用(与 Java 类中的静态成员不同)

Val t = Type.values ()[ ordinal ]

:)

我们可以用 find返回与给定谓词匹配的第一个元素,如果没有找到这样的元素,返回 null。

companion object {
fun find(value: Int): Type? = Type.values().find { it.value == value }
}

以前的一些提议的一个变体可能是以下内容,使用序数字段和 getValue:

enum class Type {
A, B, C;


companion object {
private val map = values().associateBy(Type::ordinal)


fun fromInt(number: Int): Type {
require(number in 0 until map.size) { "number out of bounds (must be positive or zero & inferior to map.size)." }
return map.getValue(number)
}
}

}

另一个示例实现。如果没有输入匹配没有枚举选项,这也会设置默认值(在这里为 OPEN) :

enum class Status(val status: Int) {
OPEN(1),
CLOSED(2);


companion object {
@JvmStatic
fun fromInt(status: Int): Status =
values().find { value -> value.status == status } ?: OPEN
}

}

另一个可能被认为更“地道”的选择是:

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

然后可以像 Type[type]一样使用它。

想出了一个更通用的解决方案

inline fun <reified T : Enum<*>> findEnumConstantFromProperty(predicate: (T) -> Boolean): T? =
T::class.java.enumConstants?.find(predicate)

示例用法:

findEnumConstantFromProperty<Type> { it.value == 1 } // Equals Type.A

没有臃肿的反射代码:

interface Identifiable<T : Number> {


val id: T
}


abstract class GettableById<T, R>(values: Array<R>) where T : Number, R : Enum<R>, R : Identifiable<T> {


private val idToValue: Map<T, R> = values.associateBy { it.id }


operator fun get(id: T): R = getById(id)


fun getById(id: T): R = idToValue.getValue(id)
}


enum class DataType(override val id: Short): Identifiable<Short> {


INT(1), FLOAT(2), STRING(3);


companion object: GettableById<Short, DataType>(values())
}


fun main() {
println(DataType.getById(1))
// or
println(DataType[2])
}

使用 null 检查和调用函数对已接受解决方案稍加扩展的方法

fun main(args: Array<String>) {
val a = Type.A // find by name
val anotherA = Type.valueOf("A") // find by name with Enums default valueOf
val aLikeAClass = Type(3) // find by value using invoke - looks like object creation


val againA = Type.of(3) // find by value
val notPossible = Type.of(6) // can result in null
val notPossibleButThrowsError = Type.ofNullSave(6) // can result in IllegalArgumentException


// prints: A, A, 0, 3
println("$a, ${a.name}, ${a.ordinal}, ${a.value}")
// prints: A, A, A null, java.lang.IllegalArgumentException: No enum constant Type with value 6
println("$anotherA, $againA, $aLikeAClass $notPossible, $notPossibleButThrowsError")
}


enum class Type(val value: Int) {
A(3),
B(4),
C(5);


companion object {
private val map = values().associateBy(Type::value)
operator fun invoke(type: Int) = ofNullSave(type)
fun of(type: Int) = map[type]
fun ofNullSave(type: Int) = map[type] ?: IllegalArgumentException("No enum constant Type with value $type")
}
}

基于你的例子,我可能会建议删除相关的值,只是使用 ordinal类似于一个索引。

序数-返回此枚举常数的序数(它在其枚举声明中的位置,其中初始常数被赋予一个零的序数)。

enum class NavInfoType {
GreenBuoy,
RedBuoy,
OtherBeacon,
Bridge,
Unknown;


companion object {
private val map = values().associateBy(NavInfoType::ordinal)
operator fun get(value: Int) = map[value] ?: Unknown
}
}

在我的例子中,如果 map返回 null,我想返回 Unknown。您还可以通过将 get替换为以下内容来引发非法参数异常:

operator fun get(value: Int) = map[value] ?: throw IllegalArgumentException()

重用代码的方法:

interface IndexedEnum {
val value: Int


companion object {
inline fun <reified T : IndexedEnum> valueOf(value: Int) =
T::class.java.takeIf { it.isEnum }?.enumConstants?.find { it.value == value }
}
}

然后可以使枚举可索引:

enum class Type(override val value: Int): IndexedEnum {
A(1),
B(2),
C(3)
}

然后像这样反向搜索:

IndexedEnum.valueOf<Type>(3)

如果您有大量枚举,这可能会节省一些按键:

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

像这样使用它:

enum class Algorithms(val string: String) {
Sha1("SHA-1"),
Sha256("SHA-256"),
}


fun main() = println(
Algorithms::string.find("SHA-256")
?: throw IllegalArgumentException("Bad algorithm string: SHA-256")
)

这将打印 Sha256

有一个完全通用的解决方案

  • 不使用反射、 Java 或 Kotlin
  • 是跨平台的,不需要任何 java
  • 麻烦最少

首先,让我们定义我们的接口,因为值字段并不是所有枚举都固有的:

interface WithValue {
val value: Int
}


interface EnumCompanion<E> where E: Enum<E> {
val map: Map<Int, E>
fun fromInt(type: Int): E = map[type] ?: throw IllegalArgumentException()
}

然后,你可以做下面的技巧

inline fun <reified E> EnumCompanion() : EnumCompanion<E>
where E : Enum<E>, E: WithValue = object : EnumCompanion<E> {
override val map: Map<Int, E> = enumValues<E>().associateBy { it.value }
}

然后,对于每个枚举,下面的代码都是有效的

enum class RGB(override val value: Int): WithValue {
RED(1), GREEN(2), BLUE(3);
companion object: EnumCompanion<RGB> by EnumCompanion()
}


val ccc = RGB.fromInt(1)


enum class Shapes(override val value: Int): WithValue {
SQUARE(22), CIRCLE(33), RECTANGLE(300);
companion object: EnumCompanion<Shapes> by EnumCompanion()
}


val zzz = Shapes.fromInt(33)

如前所述,这是不值得的,除非你有很多枚举,你真的需要得到这一般。