Эффективные перечисления в котлине с обратным поиском?

105

Я пытаюсь найти лучший способ выполнить «обратный поиск» в перечислении в Котлине. Один из моих выводов из Effective Java заключался в том, что вы вводите статическую карту внутри перечисления для обработки обратного поиска. Перенос этого на Kotlin с помощью простого перечисления приводит меня к такому коду:

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

Мой вопрос: это лучший способ сделать это или есть лучший способ? Что, если у меня есть несколько перечислений, которые следуют аналогичному шаблону? Есть ли в Kotlin способ сделать этот код более пригодным для повторного использования в перечислениях?

Барон
источник
Ваш Enum должен реализовывать идентифицируемый интерфейс со свойством id, а объект-компаньон должен расширять абстрактный класс GettableById, который содержит карту idToEnumValue и возвращает значение enum на основе идентификатора. Подробности ниже в моем ответе.
Эльдар Агаларов

Ответы:

178

Прежде всего, аргументом fromInt()должно быть не значение Int, а объект Int?. Попытка получить Typeusing null, очевидно, приведет к null, и вызывающей стороне даже не следует пытаться это сделать. MapНе имеет также никаких оснований быть изменяемым. Код можно сократить до:

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

Этот код настолько короткий, что, честно говоря, я не уверен, что стоит пытаться найти решение многократного использования.

JB Nizet
источник
8
Я собирался порекомендовать то же самое. Кроме того, я бы сделал fromIntreturn ненулевым, например Enum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26
4
Учитывая поддержку kotlin для нулевой безопасности, возвращение null из метода меня не беспокоило бы, как это было бы в Java: вызывающий будет вынужден компилятором иметь дело с возвращаемым значением null и решать, что делать (бросить или сделать что-то другое).
JB Nizet
1
@Raphael, потому что перечисления были введены в Java 5 и Optional в Java 8.
JB Nizet
2
моя версия этого кода используется by lazy{}для mapи getOrDefault()для более безопасного доступаvalue
Hoang Tran
2
Это решение работает хорошо. Обратите внимание, что для вызова Type.fromInt()из кода Java вам нужно будет аннотировать метод с помощью @JvmStatic.
Арто Бендикен
35

мы можем использовать findwhich Возвращает первый элемент, соответствующий данному предикату, или null, если такой элемент не был найден.

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}
измученный
источник
4
first { ... }Вместо этого используется очевидное усовершенствование, потому что нет смысла использовать несколько результатов.
creativecreatorormaybenot
9
Нет, использование firstне является улучшением, поскольку оно изменяет поведение и выдает ошибку, NoSuchElementExceptionесли элемент не найден, findчто равно firstOrNullвозврату null. так что, если вы хотите бросить вместо возврата null usefirst
Humazed
Этот метод можно использовать с перечислениями с несколькими значениями: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } Также вы можете генерировать исключение, если значения не находятся в перечислении: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") или вы можете использовать его при вызове этого метода: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth
Ваш метод имеет линейную сложность O (n). Лучше использовать поиск в предопределенной HashMap со сложностью O (1).
Эльдар Агаларов,
да, я знаю, но в большинстве случаев перечисление будет иметь очень небольшое количество состояний, поэтому в любом случае не имеет значения, что более читабельно.
Humazed
27

В данном случае это не имеет особого смысла, но вот "логическое извлечение" для решения @ 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

В общем, дело в том, что сопутствующие объекты можно использовать повторно (в отличие от статических членов в классе Java)

воддан
источник
Почему вы используете открытый класс? Просто сделайте это абстрактным.
Эльдар Агаларов,
21

Другой вариант, который можно было бы назвать более «идиоматическим», был бы следующим:

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

Который потом можно использовать как Type[type].

Иван Плантевин
источник
Определенно более идиоматично! Ура.
AleksandrH
6

Я обнаружил, что выполняю обратный поиск по индивидуальному заказу, вручную кодирую, значение пару раз и придумал следующий подход.

Заставить enumреализовать общий интерфейс:

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
}

Этот интерфейс (как ни странно его название :)) помечает определенное значение как явный код. Цель состоит в том, чтобы уметь писать:

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

Этого легко добиться с помощью следующего кода:

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
Это большая работа для такой простой операции, принятый ответ намного чище, ИМО
Коннор Вятт
2
Полностью согласен, простое использование определенно лучше. У меня уже был приведенный выше код для обработки явных имен для данного перечисляемого члена.
miensol
Ваш код использует отражение (плохо) и раздут (тоже плохо).
Эльдар Агаларов,
1

Вариант некоторых предыдущих предложений может быть следующим, с использованием порядкового поля и 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

Другой пример реализации. Это также устанавливает значение по умолчанию (здесь OPEN), если вход не соответствует параметру перечисления:

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

Придумал более общее решение

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

Пример использования:

findEnumConstantFromProperty<Type> { it.value == 1 } // Equals Type.A
Shalbert
источник
0

Истинный идиоматический котлинский путь. Без раздутого кода отражения:

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])
}
Эльдар Агаларов
источник
0

Немного расширенный подход принятого решения с нулевой проверкой и функцией вызова

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")
    }
}
Оливер
источник
-1

val t = Type.values ​​() [порядковый номер]

:)

shmulik.r
источник
Это работает для констант 0, 1, ..., N. Если у вас есть такие как 100, 50, 35, то это не даст правильного результата.
CoolMind