Enums efficaces dans Kotlin avec recherche inversée?

103

J'essaie de trouver le meilleur moyen de faire une «recherche inversée» sur un enum à Kotlin. L'un de mes points à retenir de Effective Java était que vous introduisiez une carte statique dans l'énumération pour gérer la recherche inversée. Porter ceci sur Kotlin avec une simple énumération me conduit à un code qui ressemble à ceci:

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

Ma question est la suivante: est-ce la meilleure façon de procéder ou existe-t-il une meilleure façon de procéder? Que faire si j'ai plusieurs énumérations qui suivent un modèle similaire? Y a-t-il un moyen dans Kotlin de rendre ce code plus réutilisable entre les énumérations?

Baron
la source
Votre Enum doit implémenter une interface identifiable avec la propriété id et l'objet compagnon doit étendre la classe abstraite GettableById qui contient la carte idToEnumValue et retourne la valeur enum basée sur id. Les détails sont ci-dessous dans ma réponse.
Eldar Agalarov le

Réponses:

177

Tout d'abord, l'argument de fromInt()devrait être un Int, pas un Int?. Essayer d'obtenir une Typeutilisation de null conduira évidemment à null, et un appelant ne devrait même pas essayer de le faire. Le Mapn'a également aucune raison d'être mutable. Le code peut être réduit à:

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

Ce code est si court que, franchement, je ne suis pas sûr que cela vaille la peine d'essayer de trouver une solution réutilisable.

JB Nizet
la source
8
J'étais sur le point de recommander la même chose. De plus, je fromIntEnum.valueOf(String)map[type] ?: throw IllegalArgumentException()
rendrais le
4
Étant donné le support kotlin pour la sécurité nulle, retourner null à partir de la méthode ne me dérangerait pas comme il le ferait en Java: l'appelant sera forcé par le compilateur de traiter une valeur renvoyée nulle et de décider quoi faire (lancer ou faire autre chose).
JB Nizet
1
@Raphael parce que les énumérations ont été introduites dans Java 5 et facultatives dans Java 8.
JB Nizet
2
ma version de ce code utilise by lazy{}pour mapet getOrDefault()pour un accès plus sûr parvalue
Hoang Tran
2
Cette solution fonctionne bien. Notez que pour pouvoir appeler à Type.fromInt()partir du code Java, vous devrez annoter la méthode avec @JvmStatic.
Arto Bendiken
35

nous pouvons utiliser findqui Renvoie le premier élément correspondant au prédicat donné, ou null si aucun élément de ce type n'a été trouvé.

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}
émerveillé
la source
4
Une amélioration évidente consiste à utiliser à la first { ... }place car il ne sert à rien de multiples résultats.
creativecreatorormaybenot
9
Non, l'utilisation firstn'est pas une amélioration car elle modifie le comportement et lance NoSuchElementExceptionsi l'élément n'est pas trouvé où findce qui est égal à firstOrNullrenvoie null. donc si vous voulez lancer au lieu de renvoyer une utilisation nullefirst
humazed
Cette méthode peut être utilisée avec des énumérations avec plusieurs valeurs: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } vous pouvez également lever une exception si les valeurs ne sont pas dans l'énumération: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") ou vous pouvez l'utiliser lors de l'appel de cette méthode: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth
Votre méthode a une complexité linéaire O (n). Mieux vaut utiliser la recherche dans HashMap prédéfini avec une complexité O (1).
Eldar Agalarov le
oui, je sais, mais dans la plupart des cas, l'énumération aura un très petit nombre d'états, donc peu importe de toute façon, ce qui est plus lisible.
humazed le
27

Cela n'a pas beaucoup de sens dans ce cas, mais voici une "extraction logique" pour la solution 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 général, c'est le problème avec les objets compagnons qu'ils peuvent être réutilisés (contrairement aux membres statiques d'une classe Java)

voddan
la source
Pourquoi utilisez-vous une classe ouverte? Faites-le simplement abstrait.
Eldar Agalarov le
21

Une autre option, qui pourrait être considérée comme plus "idiomatique", serait la suivante:

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

Qui peut ensuite être utilisé comme Type[type].

Ivan Plantevin
la source
Certainement plus idiomatique! À votre santé.
AleksandrH
6

Je me suis retrouvé à faire la recherche inversée par coutume, codé à la main, valeur deux fois et suis venu avec l'approche suivante.

Faites enumimplémenter une interface partagée:

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
}

Cette interface (aussi étrange que soit son nom :)) marque une certaine valeur comme code explicite. Le but est de pouvoir écrire:

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

Ce qui peut être facilement réalisé avec le code suivant:

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
la source
3
C'est beaucoup de travail pour une opération aussi simple, la réponse acceptée est beaucoup plus propre IMO
Connor Wyatt
2
Tout à fait d'accord pour une utilisation simple, c'est certainement mieux. J'avais déjà le code ci-dessus pour gérer les noms explicites pour un membre énuméré donné.
miensol
Votre code utilise la réflexion (mauvais) et est gonflé (mauvais aussi).
Eldar Agalarov le
1

Une variante de certaines propositions précédentes pourrait être la suivante, en utilisant un champ ordinal et 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)
    }
}

}

incise
la source
1

Un autre exemple de mise en œuvre. Cela définit également la valeur par défaut (ici à OPEN) si aucune entrée ne correspond à aucune option enum:

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
la source
0

Est venu avec une solution plus générique

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

Exemple d'utilisation:

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

True Idiomatic Kotlin Way. Sans code de réflexion gonflé:

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
la source
-1

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

:)

shmulik.r
la source
Cela fonctionne pour les constantes 0, 1, ..., N. Si vous les avez comme 100, 50, 35, alors cela ne donnera pas un bon résultat.
CoolMind