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?


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.


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

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)


¿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].


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

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

}


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
}

}


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

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

-1

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

:)


Esto funciona para las constantes 0, 1, ..., N. Si las tiene como 100, 50, 35, entonces no dará un resultado correcto.
CoolMind
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.