ChatGPT解决这个技术问题 Extra ChatGPT

Effective Enums in Kotlin with reverse lookup?

I'm trying to find the best way to do a 'reverse lookup' on an enum in Kotlin. One of my takeaways from Effective Java was that you introduce a static map inside the enum to handle the reverse lookup. Porting this over to Kotlin with a simple enum leads me to code that looks like this:

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]
        }
    }
}

My question is, is this the best way to do this, or is there a better way? What if I have several enums that follow a similar pattern? Is there a way in Kotlin to make this code more re-usable across enums?

Your Enum should implement Identifiable interface with id property and companion object should extend abstract class GettableById which holds idToEnumValue map and returns enum value based on id. Details is below in my answer.

F
Farbod Salamat-Zadeh

First of all, the argument of fromInt() should be an Int, not an Int?. Trying to get a Type using null will obviously lead to null, and a caller shouldn't even try doing that. The Map has also no reason to be mutable. The code can be reduced to:

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

That code is so short that, frankly, I'm not sure it's worth trying to find a reusable solution.


I was about to recommend the same. In addition, I would make fromInt return non-null like Enum.valueOf(String): map[type] ?: throw IllegalArgumentException()
Given the kotlin support for null-safety, returning null from the method wouldn't bother me as it would in Java: the caller will be forced by the compiler to deal with a null returned value, and decide what to do (throw or do something else).
@Raphael because enums were introduced in Java 5 and Optional in Java 8.
my version of this code use by lazy{} for the map and getOrDefault() for safer access by value
This solution works well. Note that to be able to call Type.fromInt() from Java code, you will need to annotate the method with @JvmStatic.
h
humazed

we can use find which Returns the first element matching the given predicate, or null if no such element was found.

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

An obvious enhancement is using first { ... } instead because there is no use for multiple results.
No, using first is not an enhancement as it changes the behavior and throws NoSuchElementException if the item is not found where find which is equal to firstOrNull returns null. so if you want to throw instead of returning null use first
This method can be used with enums with multiple values: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } Also you can throw an exception if the values are not in the enum: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") or you can use it when calling this method: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
Your method have linear complexity O(n). Better to use lookup in predefined HashMap with O(1) complexity.
yes, I know but in most cases, the enum will have very small number of states so it doesn't matter either way, what's more readable.
v
voddan

It makes not much sense in this case, but here is a "logic extraction" for @JBNized's solution:

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

In general that's the thing about companion objects that they can be reused (unlike static members in a Java class)


Why you use open class? Just make it abstract.
I
Ivan Plantevin

Another option, that could be considered more "idiomatic", would be the following:

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

Which can then be used like Type[type].


Definitely more idiomatic! Cheers.
s
squirrel

If you have a lot of enums, this might save a few keystrokes:

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

Use it like this:

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")
)

This will print Sha256


m
miensol

I found myself doing the reverse lookup by custom, hand coded, value couple of times and came of up with following approach.

Make enums implement a shared interface:

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
}

This interface (however strange the name is :)) marks a certain value as the explicit code. The goal is to be able to write:

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

Which can easily be achieved with the following code:

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)

That's a lot of work for such a simple operation, the accepted answer is much cleaner IMO
Fully agree for simple use it's definitely better. I had the above code already to handle explicit names for given enumerated member.
Your code using reflection (bad) and is bloated (bad too).
T
Tormod Haugene

Another example implementation. This also sets the default value (here to OPEN) if no the input matches no enum option:

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
}

}


This solution performs well and gives the option to provide a default value or ?: throw IllegalArgumentException(status.toString())
E
Eldar Agalarov

True Idiomatic Kotlin Way. Without bloated reflection code:

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])
}

i
incises

A variant of some previous proposals might be the following, using ordinal field and 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)
    }
}

}


O
Oliver

A slightly extended approach of the accepted solution with null check and invoke function

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")
    }
}

G
Giordano

An approach that reuses code:

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 }
    }
}

Then the enums can be made indexable:

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

and reverse searched like so:

IndexedEnum.valueOf<Type>(3)

M
Mikhail Belyaev

There is a completely generic solution that

Does not use reflection, Java or Kotlin

Is cross-platform, does not need any java

Has minimum hassle

First, let's define our interfaces as value field is not inherent to all enums:

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()
}

Then, you can do the following trick

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 }
}

Then, for every enum you have the following just works

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)

As already mentioned, this is not worth it unless you have a lot of enums and you really need to get this generic.


S
Shalbert

Came up with a more generic solution

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

Example usage:

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

J
James

Based on your example, i might suggest removing the associated value and just use the ordinal which is similar to an index.

ordinal - Returns the ordinal of this enumeration constant (its position in its enum declaration, where the initial constant is assigned an ordinal of zero).

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
    }
}

In my case i wanted to return Unknown if the map returned null. You could also throw an illegal argument exception by replacing the get with the following:

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

s
shmulik.r

val t = Type.values()[ordinal]

:)


This works for constants 0, 1, ..., N. If you have them like 100, 50, 35, then it won't give a right result.