Enum efficaci in Kotlin con ricerca inversa?


102

Sto cercando di trovare il modo migliore per eseguire una "ricerca inversa" su un enum in Kotlin. Uno dei miei suggerimenti da Effective Java è stato l'introduzione di una mappa statica all'interno dell'enum per gestire la ricerca inversa. Portare questo su Kotlin con una semplice enumerazione mi porta a un codice simile a questo:

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

La mia domanda è: è questo il modo migliore per farlo, o c'è un modo migliore? Cosa succede se ho più enumerazioni che seguono uno schema simile? C'è un modo in Kotlin per rendere questo codice più riutilizzabile tra le enumerazioni?


Il tuo Enum dovrebbe implementare l'interfaccia Identifiable con la proprietà id e l'oggetto associato dovrebbe estendere la classe astratta GettyById che contiene la mappa idToEnumValue e restituisce il valore enum basato su id. I dettagli sono di seguito nella mia risposta.
Eldar Agalarov

Risposte:


176

Prima di tutto, l'argomento di fromInt()dovrebbe essere an Int, non an Int?. Il tentativo di ottenere un valore Typenull porterà ovviamente a null e un chiamante non dovrebbe nemmeno provare a farlo. L' Mapha anche ragione di essere mutevole. Il codice può essere ridotto a:

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

Quel codice è così breve che, francamente, non sono sicuro che valga la pena provare a trovare una soluzione riutilizzabile.


8
Stavo per raccomandare lo stesso. Inoltre, farei un reso fromIntnon nullo come Enum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26

4
Dato il supporto di kotlin per null-safety, restituire null dal metodo non mi infastidirebbe come in Java: il chiamante sarà costretto dal compilatore a gestire un valore restituito null e decidere cosa fare (lanciare o fare qualcos'altro).
JB Nizet

1
@Raphael perché le enumerazioni sono state introdotte in Java 5 e facoltative in Java 8.
JB Nizet

2
la mia versione di questo codice utilizzato by lazy{}per mape getOrDefault()per un accesso più sicuro davalue
Hoang Tran

2
Questa soluzione funziona bene. Nota che per poter chiamare Type.fromInt()dal codice Java, dovrai annotare il metodo con @JvmStatic.
Arto Bendiken

34

possiamo usare findwhich Restituisce il primo elemento che corrisponde al predicato dato, o null se non è stato trovato alcun elemento di questo tipo.

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

4
Un ovvio miglioramento sta first { ... }invece usando perché non è utile per più risultati.
creativecreatorormaybenot

9
No, l'uso firstnon è un miglioramento in quanto cambia il comportamento e genera NoSuchElementExceptionse l'elemento non viene trovato dove findè uguale a firstOrNullritorni null. quindi se vuoi lanciare invece di restituire un uso nullofirst
umiliato il

Questo metodo può essere usato con enumerazioni con più valori: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } Inoltre puoi lanciare un'eccezione se i valori non sono nell'enumerazione: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") oppure puoi usarla quando chiami questo metodo: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth

Il tuo metodo ha complessità lineare O (n). Meglio usare la ricerca in HashMap predefinita con complessità O (1).
Eldar Agalarov

sì, lo so, ma nella maggior parte dei casi, l'enum avrà un numero molto piccolo di stati quindi non importa in entrambi i casi, cosa c'è di più leggibile.
umiliato il

27

Non ha molto senso in questo caso, ma ecco una "estrazione logica" per la soluzione di @ 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

In generale è questo il problema degli oggetti companion che possono essere riutilizzati (a differenza dei membri statici in una classe Java)


Perché usi la classe aperta? Rendilo astratto.
Eldar Agalarov

21

Un'altra opzione, che potrebbe essere considerata più "idiomatica", sarebbe la seguente:

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

Che può quindi essere utilizzato come Type[type].


Decisamente più idiomatico! Saluti.
AleksandrH

6

Mi sono ritrovato a fare la ricerca inversa per un paio di volte personalizzato, codificato a mano, valore e ho trovato il seguente approccio.

Fai enumimplementare un'interfaccia condivisa:

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
}

Questa interfaccia (per quanto strano sia il nome :)) contrassegna un certo valore come codice esplicito. L'obiettivo è riuscire a scrivere:

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

Che può essere facilmente ottenuto con il seguente codice:

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
È molto lavoro per un'operazione così semplice, la risposta accettata è molto più pulita IMO
Connor Wyatt

2
Pienamente d'accordo per un uso semplice è decisamente meglio. Avevo già il codice sopra per gestire i nomi espliciti per un dato membro enumerato.
miensol

Il tuo codice utilizza la riflessione (cattivo) ed è gonfio (anche cattivo).
Eldar Agalarov

1

Una variante di alcune proposte precedenti potrebbe essere la seguente, utilizzando il campo ordinale e 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

Un altro esempio di implementazione. Questo imposta anche il valore predefinito (qui a OPEN) se l'input non corrisponde a nessuna opzione 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
}

}


0

È venuto con una soluzione più generica

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

Utilizzo di esempio:

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

0

True Idiomatic Kotlin Way. Senza codice di riflessione gonfio:

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]

:)


Questo funziona per le costanti 0, 1, ..., N. Se li hai come 100, 50, 35, allora non darà un risultato corretto.
CoolMind
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.