Effektive Enums in Kotlin mit Reverse Lookup?

101

Ich versuche, den besten Weg zu finden, um eine Enum-Suche in einer Aufzählung in Kotlin durchzuführen. Eine meiner Erkenntnisse aus Effective Java war, dass Sie eine statische Karte in die Enumeration einführen, um die umgekehrte Suche durchzuführen. Wenn ich dies mit einer einfachen Aufzählung nach Kotlin portiere, komme ich zu Code, der so aussieht:

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

Meine Frage ist, ist dies der beste Weg, oder gibt es einen besseren Weg? Was ist, wenn ich mehrere Aufzählungen habe, die einem ähnlichen Muster folgen? Gibt es in Kotlin eine Möglichkeit, diesen Code über Aufzählungen hinweg wiederverwendbarer zu machen?

Baron
quelle
Ihre Aufzählung sollte eine identifizierbare Schnittstelle mit der Eigenschaft id implementieren, und das Begleitobjekt sollte die abstrakte Klasse GettableById erweitern, die die Zuordnung idToEnumValue enthält und einen Aufzählungswert basierend auf der ID zurückgibt. Details finden Sie unten in meiner Antwort.
Eldar Agalarov

Antworten:

173

Zuallererst sollte das Argument von fromInt()ein sein Int, nicht ein Int?. Der Versuch, eine Typeusing-Null zu erhalten, führt offensichtlich zu null, und ein Anrufer sollte dies nicht einmal versuchen. Das Maphat auch keinen Grund, veränderlich zu sein. Der Code kann reduziert werden auf:

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

Dieser Code ist so kurz, dass ich ehrlich gesagt nicht sicher bin, ob es sich lohnt, eine wiederverwendbare Lösung zu finden.

JB Nizet
quelle
8
Ich wollte das gleiche empfehlen. Außerdem würde ich die fromIntRückgabe ungleich Null machen wie Enum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26
4
Angesichts der Kotlin-Unterstützung für Null-Sicherheit würde mich die Rückgabe von Null von der Methode nicht wie in Java stören: Der Aufrufer wird vom Compiler gezwungen, mit einem Null-Rückgabewert umzugehen und zu entscheiden, was zu tun ist (werfen oder tun) etwas anderes).
JB Nizet
1
@ Raphael, weil Aufzählungen in Java 5 und optional in Java 8 eingeführt wurden.
JB Nizet
2
Meine Version dieses Codes wird by lazy{}für den mapund getOrDefault()für einen sichereren Zugriff vonvalue
Hoang Tran
2
Diese Lösung funktioniert gut. Beachten Sie, dass Type.fromInt()Sie die Methode mit Anmerkungen versehen müssen, um aus Java-Code aufrufen zu können @JvmStatic.
Arto Bendiken
34

Wir können verwenden, findwelches das erste Element zurückgibt, das mit dem angegebenen Prädikat übereinstimmt, oder null, wenn kein solches Element gefunden wurde.

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}
humazed
quelle
4
Eine offensichtliche Verbesserung ist first { ... }stattdessen die Verwendung, da mehrere Ergebnisse nicht verwendet werden können.
CreativeCreatorormaybenot
9
Nein, die Verwendung firstist keine Verbesserung, da sie das Verhalten ändert und auslöst, NoSuchElementExceptionwenn das Element nicht dort gefunden wird, wo es findder firstOrNullRückgabe entspricht null. first
Also
Diese Methode kann mit Aufzählungen mit mehreren Werten verwendet werden: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } Sie können auch eine Ausnahme auslösen, wenn die Werte nicht in der Aufzählung enthalten sind: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") oder Sie können sie verwenden, wenn Sie diese Methode aufrufen: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth
Ihre Methode hat die lineare Komplexität O (n). Verwenden Sie die Suche besser in vordefinierter HashMap mit O (1) -Komplexität.
Eldar Agalarov
Ja, ich weiß, aber in den meisten Fällen wird die Aufzählung eine sehr kleine Anzahl von Zuständen haben, so dass es egal ist, was besser lesbar ist.
Humazed
27

In diesem Fall macht es nicht viel Sinn, aber hier ist eine "Logikextraktion" für die Lösung von @ 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

Im Allgemeinen ist dies die Sache bei Begleitobjekten, die wiederverwendet werden können (im Gegensatz zu statischen Elementen in einer Java-Klasse).

Voddan
quelle
Warum benutzt du offene Klasse? Mach es einfach abstrakt.
Eldar Agalarov
21

Eine andere Option, die als "idiomatischer" angesehen werden könnte, wäre die folgende:

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

Welches kann dann wie verwendet werden Type[type].

Ivan Plantevin
quelle
Auf jeden Fall idiomatischer! Prost.
AleksandrH
6

Ich fand mich dabei wieder, die umgekehrte Suche durch benutzerdefinierte, handcodierte Werte einige Male durchzuführen, und fand den folgenden Ansatz.

Lassen Sie enums eine gemeinsame Schnittstelle implementieren:

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
}

Diese Schnittstelle (wie seltsam der Name auch sein mag :)) markiert einen bestimmten Wert als expliziten Code. Das Ziel ist es, schreiben zu können:

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

Was mit dem folgenden Code leicht erreicht werden kann:

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
quelle
3
Das ist eine Menge Arbeit für eine so einfache Operation, die akzeptierte Antwort ist viel sauberer IMO
Connor Wyatt
2
Stimmen Sie voll und ganz für den einfachen Gebrauch zu, es ist definitiv besser. Ich hatte den obigen Code bereits, um explizite Namen für ein bestimmtes aufgezähltes Mitglied zu verarbeiten.
Miensol
Ihr Code verwendet Reflection (schlecht) und ist aufgebläht (auch schlecht).
Eldar Agalarov
1

Eine Variante einiger früherer Vorschläge könnte die folgende sein, wobei Ordnungsfeld und getValue verwendet werden:

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

}}

schneidet ein
quelle
1

Eine weitere Beispielimplementierung. Dies setzt auch den Standardwert (hier auf OPEN), wenn nein, die Eingabe stimmt mit keiner Aufzählungsoption überein:

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
quelle
0

Hat eine allgemeinere Lösung gefunden

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

Anwendungsbeispiel:

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

True Idiomatic Kotlin Way. Ohne aufgeblähten Reflexionscode:

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
quelle
-1

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

:) :)

shmulik.r
quelle
Dies funktioniert für die Konstanten 0, 1, ..., N. Wenn Sie sie wie 100, 50, 35 haben, führt dies nicht zu einem richtigen Ergebnis.
CoolMind