Avatar ·

Эффективные перечисления в Kotlin: как сделать обратный поиск по значению?

Всем привет! Я пишу на Kotlin недавно и столкнулся с такой задачей. У меня есть enum класс для статусов заказа: ```kotlin enum class OrderStatus(val code: Int) { PENDING(1), PROCESSING(2), SHIPPED(3), DELIVERED(4), CANCELLED(5) } ``` Из базы данных я получаю числовой код статуса (например, 3), и мне нужно получить соответствующее значение enum — `OrderStatus.SHIPPED`. Я знаю, что в Java часто делают статический `HashMap` для обратного поиска. Но в Kotlin это выглядит громоздко. Я пробовал написать функцию-расширение: ```kotlin fun OrderStatus.Companion.fromCode(code: Int): OrderStatus? { return values().find { it.code == code } } ``` Но это работает медленно, потому что каждый раз проходит по всем значениям. Для 5 статусов это не критично, но в проекте есть enum с 30+ значениями, и я боюсь, что на больших данных это будет тормозить. Вопрос: как правильно реализовать эффективный обратный поиск (reverse lookup) в Kotlin enums? Есть ли встроенные средства или идиоматичный способ? Хочется, чтобы было быстро, компактно и без лишнего кода. Спасибо! <<>>

Оптимальное решение: ленивая инициализация маппинга через companion object

В Kotlin лучший способ реализовать обратный поиск — создать статическую мапу внутри companion object с ленивой инициализацией. Это даёт O(1) время доступа и не требует перебора всех значений при каждом запросе.

enum class OrderStatus(val code: Int) {
    PENDING(1),
    PROCESSING(2),
    SHIPPED(3),
    DELIVERED(4),
    CANCELLED(5);

    companion object {
        private val map: Map<Int, OrderStatus> by lazy {
            values().associateBy { it.code }
        }

        fun fromCode(code: Int): OrderStatus? = map[code]
    }
}

Как это работает:

  • by lazy — мапа создаётся только при первом обращении к fromCode(), а не при загрузке класса.
  • associateBy { it.code } — строит HashMap, где ключ — это code, а значение — сам enum.
  • Доступ по ключу в HashMap — O(1), что идеально для больших enum.

Почему это лучше вашего варианта?

Ваша функция values().find { ... } каждый раз проходит по всем элементам (O(n)). Для 5 значений разница незаметна, но для 50+ она может быть существенной, особенно если вызывать fromCode тысячи раз в секунду. Мапа решает эту проблему раз и навсегда.

Дополнительные улучшения

Если вы хотите избежать null и выбрасывать исключение при неизвестном коде:

fun fromCode(code: Int): OrderStatus = 
    map[code] ?: throw IllegalArgumentException("Unknown code: $code")

Или если хотите более безопасный API с возможностью задать значение по умолчанию:

fun fromCode(code: Int, default: OrderStatus = PENDING): OrderStatus = 
    map[code] ?: default

Альтернативный синтаксис (без lazy)

Если enum небольшой или загружается редко, можно без lazy:

companion object {
    private val map = values().associateBy { it.code }
    fun fromCode(code: Int) = map[code]
}

Разница: мапа создаётся сразу при инициализации класса (при первом обращении к любому методу enum). lazy откладывает создание до первого вызова fromCode.

Для enum с разными типами ключей

Шаблон универсален для любых типов (String, Long, UUID и т.д.):

enum class Priority(val level: String) {
    LOW("low"),
    MEDIUM("medium"),
    HIGH("high");

    companion object {
        private val map = values().associateBy { it.level }
        fun fromLevel(level: String) = map[level]
    }
}

Этот подход считается идиоматичным для Kotlin и рекомендуется в официальных гайдлайнах. Он компактный, эффективный и легко читается.

<<>>

Альтернатива: использование sealed class вместо enum

Если ваш случай сложнее, чем просто числовые коды (например, нужно хранить дополнительные данные или иметь разную логику для каждого значения), можно рассмотреть sealed class с объектами:

sealed class OrderStatus(val code: Int) {
    object PENDING : OrderStatus(1)
    object PROCESSING : OrderStatus(2)
    object SHIPPED : OrderStatus(3)
    object DELIVERED : OrderStatus(4)
    object CANCELLED : OrderStatus(5)

    companion object {
        private val map = listOf(
            PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED
        ).associateBy { it.code }

        fun fromCode(code: Int): OrderStatus? = map[code]
    }
}

Плюсы:

  • Можно наследовать разные классы с разной реализацией методов.
  • Больше гибкости для сложной логики.

Минусы:

  • Нет встроенного values() — нужно вручную перечислять объекты.
  • Нельзя использовать в when без else (хотя можно добавить абстрактный метод).

Для простого случая с кодами лучше оставить enum — он лаконичнее и имеет встроенную поддержку values() и valueOf().