Ефективні Enums у Котліні із зворотним пошуком?


103

Я намагаюся знайти найкращий спосіб зробити "зворотний пошук" на перерахунок у Котліні. Одним із моїх виходів з Ефективної Java було те, що ви вводите статичну карту всередині enum для обробки зворотного пошуку. Перенесення цього котліна з простим перерахуванням призводить мене до коду, який виглядає приблизно так:

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

Моє запитання: це найкращий спосіб зробити це чи є кращий спосіб? Що робити, якщо у мене є кілька перерахунків, які відповідають подібній схемі? Чи є спосіб у Котліні зробити цей код більш повторним для використання через переліки?


Ваш Enum повинен реалізувати інтерфейс, що може ідентифікуватися, з властивістю id, а об'єкт супутника повинен розширювати абстрактний клас GettableById, який містить карту idToEnumValue і повертає значення enum на основі id. Деталі нижче в моїй відповіді.
Ельдар Агаларов

Відповіді:


177

Перш за все, аргументом fromInt()слід бути an Int, а не an Int?. Спроба отримати Typeнульовий параметр очевидно призведе до нуля, і абонент навіть не повинен намагатися це робити. Це Mapтакож не має підстав бути зміненим. Код можна зменшити до:

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

Цей код настільки короткий, що, чесно кажучи, я не впевнений, що варто спробувати знайти рішення для багаторазового використання.


8
Я збирався рекомендувати те саме. Крім того, я зробив би fromIntповернення ненульовим на кшталт Enum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26

4
Враховуючи підтримку kotlin для захисту від нуля, повернення null з методу не турбуватиме мене так, як на Java: компілятор буде змушений компілятором працювати з поверненим значенням null і вирішувати, що робити (кидати чи робити щось ще).
JB Nizet

1
@Raphael тому, що переписки були введені на Java 5 та необов’язково на Java 8.
JB Nizet

2
моя версія цього використання коду by lazy{}для mapі getOrDefault()для безпечного доступу,value
Хоанг Tran

2
Це рішення добре працює. Зауважте, що для того, щоб мати можливість зателефонувати Type.fromInt()з коду Java, вам потрібно буде анотувати метод @JvmStatic.
Арто Бендікен

35

ми можемо використовувати findякий повертає перший елемент, що відповідає заданому предикату, або null, якщо такого елемента не було знайдено.

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

4
Очевидне вдосконалення використовує first { ... }натомість, оскільки немає корисності для кількох результатів.
creativecreatorormaybenot

9
Ні, використання firstне є розширенням, оскільки воно змінює поведінку і кидає, NoSuchElementExceptionякщо елемент не знайдеться там, findде дорівнює firstOrNullвіддачі null. тож якщо ви хочете кинути замість повернення нульового використанняfirst
гумав

Цей метод можна використовувати з перерахунками з декількома значеннями: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } Також ви можете викинути виняток, якщо значення не знаходяться в enum: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") або ви можете використовувати його під час виклику цього методу: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth

Ваш метод має лінійну складність O (n). Краще використовувати пошук у заздалегідь заданому HashMap зі складністю O (1).
Ельдар Агаларов

так, я знаю, але в більшості випадків у перерахунку буде дуже мала кількість станів, тому не важливо в будь-якому випадку, що можна читати.
гумав

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


Однозначно ідіоматичніше! Ура.
ОлександрХ

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
Це дуже багато роботи для такої простої операції, прийнята відповідь набагато чіткіша IMO
Коннор Віат

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

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

-1

val t = Тип.значення () [порядковий]

:)


Це працює для констант 0, 1, ..., N. Якщо у вас є 100, 50, 35, це не дасть правильного результату.
CoolMind
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.