Skuteczne wyliczenia w Kotlin z odwrotnym wyszukiwaniem?

105

Próbuję znaleźć najlepszy sposób na „wyszukiwanie wsteczne” wyliczenia w Kotlinie. Jednym z moich wniosków z Effective Java było wprowadzenie statycznej mapy do wyliczenia w celu obsługi wyszukiwania wstecznego. Przeniesienie tego do Kotlina za pomocą prostego wyliczenia prowadzi mnie do kodu, który wygląda następująco:

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

Moje pytanie brzmi: czy to najlepszy sposób, czy jest lepszy sposób? A co, jeśli mam kilka wyliczeń, które mają podobny wzór? Czy w Kotlinie jest sposób, aby ten kod był bardziej nadający się do ponownego wykorzystania w wyliczeniach?

Baron
źródło
Twój Enum powinien implementować Identyfikowalny interfejs z właściwością id, a obiekt towarzyszący powinien rozszerzać klasę abstrakcyjną GettableById, która zawiera mapę idToEnumValue i zwraca wartość wyliczenia na podstawie id. Szczegóły poniżej w mojej odpowiedzi.
Eldar Agalarov

Odpowiedzi:

178

Przede wszystkim argument fromInt()powinien być an Int, a nie Int?. Próba uzyskania Typeusing null oczywiście prowadzi do null, a dzwoniący nie powinien nawet próbować tego robić. Nie Mapma również powodu, aby być zmiennym. Kod można zredukować do:

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

Ten kod jest tak krótki, że, szczerze mówiąc, nie jestem pewien, czy warto szukać rozwiązania wielokrotnego użytku.

JB Nizet
źródło
8
Miałem to samo polecić. Ponadto, sprawiłbym, że fromIntzwrot nie jest zerowy, jak Enum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26
4
Biorąc pod uwagę obsługę kotlin dla bezpieczeństwa null, zwrócenie wartości null z metody nie przeszkadzałoby mi tak, jak w Javie: wywołujący zostanie zmuszony przez kompilator do zajęcia się wartością zwracaną przez null i zdecyduje, co zrobić (wyrzucić lub zrobić coś innego).
JB Nizet
1
@Raphael, ponieważ wyliczenia zostały wprowadzone w Javie 5 i Opcjonalnie w Javie 8.
JB Nizet
2
moja wersja tego kodu służy by lazy{}do mapi getOrDefault()dla bezpieczniejszego dostępu przezvalue
Hoang Tran
2
To rozwiązanie działa dobrze. Zauważ, że aby móc wywoływać Type.fromInt()z kodu Java, musisz dodać adnotację do metody @JvmStatic.
Arto Bendiken
35

możemy użyć findwhich Zwraca pierwszy element pasujący do danego predykatu lub null, jeśli takiego elementu nie znaleziono.

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}
humazed
źródło
4
first { ... }Zamiast tego używane jest oczywiste ulepszenie, ponieważ nie ma potrzeby uzyskiwania wielu wyników.
creativecreatorormaybenot
9
Nie, użycie firstnie jest ulepszeniem, ponieważ zmienia zachowanie i wyrzuca, NoSuchElementExceptionjeśli element nie zostanie znaleziony, gdzie findjest równy firstOrNullzwrotowi null. więc jeśli chcesz rzucić zamiast zwracać null użyjfirst
humazed
Ta metoda może być używana z wyliczeniami z wieloma wartościami: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } Możesz również zgłosić wyjątek, jeśli wartości nie ma w wyliczeniu: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") lub możesz go użyć podczas wywoływania tej metody: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth
Twoja metoda ma złożoność liniową O (n). Lepiej jest używać wyszukiwania we wstępnie zdefiniowanej HashMap ze złożonością O (1).
Eldar Agalarov
tak, wiem, ale w większości przypadków wyliczenie będzie miało bardzo małą liczbę stanów, więc nie ma to żadnego znaczenia, co jest bardziej czytelne.
humazed
27

W tym przypadku nie ma to większego sensu, ale oto „wyodrębnianie logiki” dla rozwiązania @ 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

Ogólnie rzecz biorąc, chodzi o obiekty towarzyszące, które można ponownie wykorzystać (w przeciwieństwie do statycznych elementów członkowskich w klasie Java)

voddan
źródło
Dlaczego korzystasz z klasy otwartej? Po prostu uczyń to abstrakcyjnym.
Eldar Agalarov
21

Inna opcja, którą można by uznać za bardziej „idiomatyczną”, byłaby następująca:

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

Które można następnie wykorzystać jak Type[type].

Ivan Plantevin
źródło
Zdecydowanie bardziej idiomatyczne! Twoje zdrowie.
AleksandrH
6

Znalazłem się kilka razy, wykonując wyszukiwanie wsteczne przez niestandardowe, ręcznie kodowane, wartości i wymyśliłem następujące podejście.

Make enums implementuje wspólny interfejs:

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
}

Ten interfejs (jakkolwiek dziwna nazwa to :)) oznacza określoną wartość jako jawny kod. Celem jest umiejętność pisania:

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

Co można łatwo osiągnąć za pomocą następującego kodu:

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
źródło
3
To dużo pracy jak na tak prostą operację, przyjęta odpowiedź jest dużo czystsza IMO
Connor Wyatt
2
W pełni zgadzam się na prostą obsługę, zdecydowanie lepiej. Miałem już powyższy kod do obsługi jawnych nazw dla danego wyliczonego członka.
miensol
Twój kod używa odbicia (źle) i jest rozdęty (też zły).
Eldar Agalarov
1

Wariantem niektórych poprzednich propozycji może być następujący, wykorzystujący pole porządkowe i 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)
    }
}

}

nacięcia
źródło
1

Kolejna przykładowa realizacja. Spowoduje to również ustawienie wartości domyślnej (tutaj na OPEN), jeśli żadne dane wejściowe nie pasują do żadnej opcji wyliczenia:

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
źródło
0

Pojawił się z bardziej ogólnym rozwiązaniem

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

Przykładowe użycie:

findEnumConstantFromProperty<Type> { it.value == 1 } // Equals Type.A
Shalbert
źródło
0

True Idiomatic Kotlin Way. Bez nadętego kodu odbicia:

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
źródło
0

Nieco rozszerzone podejście do przyjętego rozwiązania z funkcją sprawdzania wartości null i wywołania

fun main(args: Array<String>) {
    val a = Type.A // find by name
    val anotherA = Type.valueOf("A") // find by name with Enums default valueOf
    val aLikeAClass = Type(3) // find by value using invoke - looks like object creation

    val againA = Type.of(3) // find by value
    val notPossible = Type.of(6) // can result in null
    val notPossibleButThrowsError = Type.ofNullSave(6) // can result in IllegalArgumentException

    // prints: A, A, 0, 3
    println("$a, ${a.name}, ${a.ordinal}, ${a.value}")
    // prints: A, A, A null, java.lang.IllegalArgumentException: No enum constant Type with value 6
    println("$anotherA, $againA, $aLikeAClass $notPossible, $notPossibleButThrowsError")
}

enum class Type(val value: Int) {
    A(3),
    B(4),
    C(5);

    companion object {
        private val map = values().associateBy(Type::value)
        operator fun invoke(type: Int) = ofNullSave(type)
        fun of(type: Int) = map[type]
        fun ofNullSave(type: Int) = map[type] ?: IllegalArgumentException("No enum constant Type with value $type")
    }
}
Oliver
źródło
-1

val t = Type.values ​​() [porządkowa]

:)

shmulik.r
źródło
Działa to dla stałych 0, 1, ..., N. Jeśli masz je jak 100, 50, 35, to nie da to prawidłowego wyniku.
CoolMind