Enums efectivos en Kotlin con búsqueda inversa?

102

Estoy tratando de encontrar la mejor manera de hacer una 'búsqueda inversa' en una enumeración en Kotlin. Una de mis conclusiones de Effective Java fue que introduces un mapa estático dentro de la enumeración para manejar la búsqueda inversa. Transferir esto a Kotlin con una enumeración simple me lleva a un código que se ve así:

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

Mi pregunta es, ¿es esta la mejor manera de hacer esto o hay una mejor manera? ¿Qué pasa si tengo varias enumeraciones que siguen un patrón similar? ¿Hay alguna forma en Kotlin de hacer que este código sea más reutilizable entre enumeraciones?

Barón
fuente
Su Enum debe implementar una interfaz identificable con la propiedad id y el objeto complementario debe extender la clase abstracta GettableById que contiene el mapa idToEnumValue y devuelve el valor de enum basado en id. Los detalles se encuentran a continuación en mi respuesta.
Eldar Agalarov

Respuestas:

176

En primer lugar, el argumento de fromInt()debería ser un Int, no un Int?. Intentar obtener un Typeuso nulo obviamente conducirá a un valor nulo, y la persona que llama ni siquiera debería intentar hacerlo. El Maptambién tiene ninguna razón de ser mutable. El código se puede reducir a:

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

Ese código es tan corto que, francamente, no estoy seguro de que valga la pena intentar encontrar una solución reutilizable.

JB Nizet
fuente
8
Estaba a punto de recomendar lo mismo. Además, haría un fromIntretorno no nulo como Enum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26
4
Dado el soporte de kotlin para seguridad nula, devolver nulo desde el método no me molestaría como lo haría en Java: el compilador forzará a la persona que llama a lidiar con un valor devuelto nulo y decidir qué hacer (lanzar o hacer algo más).
JB Nizet
1
@Raphael porque las enumeraciones se introdujeron en Java 5 y Opcional en Java 8.
JB Nizet
2
mi versión de este código se usa by lazy{}para mapy getOrDefault()para un acceso más seguro porvalue
Hoang Tran
2
Esta solución funciona bien. Tenga en cuenta que para poder llamar Type.fromInt()desde el código Java, deberá anotar el método con @JvmStatic.
Arto Bendiken
34

podemos usar findwhich Devuelve el primer elemento que coincide con el predicado dado, o nulo si no se encontró dicho elemento.

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}
humazed
fuente
4
En su first { ... }lugar, se utiliza una mejora obvia porque no se utilizan resultados múltiples.
creativecreatorormaybenot
9
No, el uso firstno es una mejora, ya que cambia el comportamiento y arroja NoSuchElementExceptionsi el elemento no se encuentra, lo findque equivale a firstOrNulldevoluciones null. así que si desea lanzar en lugar de devolver el uso nulofirst
humazed
Este método se puede usar con enumeraciones con múltiples valores: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } también puede lanzar una excepción si los valores no están en la enumeración: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") o puede usarlo al llamar a este método: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth
Su método tiene complejidad lineal O (n). Es mejor usar la búsqueda en HashMap predefinido con complejidad O (1).
Eldar Agalarov
sí, lo sé, pero en la mayoría de los casos, la enumeración tendrá una cantidad muy pequeña de estados, por lo que no importa de ninguna manera, lo que es más legible.
humazed
27

No tiene mucho sentido en este caso, pero aquí hay una "extracción lógica" para la solución de @ 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

En general, eso es lo que pasa con los objetos complementarios que se pueden reutilizar (a diferencia de los miembros estáticos en una clase Java)

voddan
fuente
¿Por qué usas la clase abierta? Hazlo abstracto.
Eldar Agalarov
21

Otra opción, que podría considerarse más "idiomática", sería la siguiente:

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

Que luego se puede usar como Type[type].

Ivan Plantevin
fuente
¡Definitivamente más idiomático! Salud.
AleksandrH
6

Me encontré haciendo la búsqueda inversa por valor personalizado, codificado a mano, un par de veces y se me ocurrió el siguiente enfoque.

Haga que enums implemente una interfaz compartida:

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
}

Esta interfaz (por extraño que sea el nombre :)) marca un cierto valor como código explícito. El objetivo es poder escribir:

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

Lo cual se puede lograr fácilmente con el siguiente código:

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)
miensol
fuente
3
Eso es mucho trabajo para una operación tan simple, la respuesta aceptada es mucho más limpia OMI
Connor Wyatt
2
Totalmente de acuerdo para un uso simple, definitivamente es mejor. Ya tenía el código anterior para manejar nombres explícitos para un miembro enumerado dado.
miensol
Su código usa reflexión (malo) y está hinchado (malo también).
Eldar Agalarov
1

Una variante de algunas propuestas anteriores podría ser la siguiente, utilizando el campo ordinal y 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)
    }
}

}

incisiones
fuente
1

Otro ejemplo de implementación. Esto también establece el valor predeterminado (aquí para OPEN) si no la entrada coincide con ninguna opción de enumeración:

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
}

}

Tormod Haugene
fuente
0

Se me ocurrió una solución más genérica

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

Uso de ejemplo:

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

Verdadera manera idiomática de Kotlin. Sin código de reflexión hinchado:

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])
}
Eldar Agalarov
fuente
-1

val t = Type.values ​​() [ordinal]

:)

shmulik.r
fuente
Esto funciona para las constantes 0, 1, ..., N. Si las tiene como 100, 50, 35, entonces no dará un resultado correcto.
CoolMind