Enums eficazes em Kotlin com pesquisa reversa?

102

Estou tentando encontrar a melhor maneira de fazer uma 'pesquisa reversa' em um enum em Kotlin. Uma das minhas lições do Effective Java foi que você introduz um mapa estático dentro do enum para lidar com a pesquisa reversa. Transferir isso para Kotlin com uma enum simples me leva a um código semelhante a este:

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

Minha pergunta é: essa é a melhor maneira de fazer isso ou existe uma maneira melhor? E se eu tiver vários enums que seguem um padrão semelhante? Existe uma maneira no Kotlin de tornar esse código mais reutilizável em enums?

Barão
fonte
Seu Enum deve implementar uma interface identificável com propriedade id e o objeto complementar deve estender a classe abstrata GettableById que contém o mapa idToEnumValue e retorna o valor enum com base no id. Os detalhes estão abaixo na minha resposta.
Eldar Agalarov

Respostas:

176

Em primeiro lugar, o argumento de fromInt()deveria ser um Int, não um Int?. Tentar obter um Typeusando null obviamente levará a null, e um chamador nem deveria tentar fazer isso. O Maptambém não tem razão para ser mutável. O código pode ser reduzido a:

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

Esse código é tão curto que, francamente, não tenho certeza se vale a pena tentar encontrar uma solução reutilizável.

JB Nizet
fonte
8
Eu estava prestes a recomendar o mesmo. Além disso, eu faria o fromIntretorno não nulo, como Enum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26
4
Dado o suporte kotlin para null-safety, retornar null do método não me incomodaria como faria em Java: o chamador será forçado pelo compilador a lidar com um valor nulo retornado e decidir o que fazer (lançar ou fazer algo mais).
JB Nizet
1
@Raphael porque os enums foram introduzidos no Java 5 e opcionais no Java 8.
JB Nizet
2
minha versão deste código usa by lazy{}para o mape getOrDefault()para um acesso mais segurovalue
Hoang Tran
2
Esta solução funciona bem. Observe que para poder chamar a Type.fromInt()partir do código Java, você precisará anotar o método com @JvmStatic.
Arto Bendiken
34

podemos usar findwhich Returns o primeiro elemento correspondente ao predicado fornecido, ou null se nenhum elemento for encontrado.

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}
humazed
fonte
4
Um aprimoramento óbvio é usar first { ... }em vez disso, porque não há uso para vários resultados.
creativecreatorormaybenot
9
Não, usar firstnão é um aprimoramento, pois muda o comportamento e atira NoSuchElementExceptionse o item não for encontrado onde o findque é igual a firstOrNullretorna null. então, se você quiser lançar em vez de retornar o uso nulofirst
humazed 01 de
Este método pode ser usado com enums com vários valores: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } Além disso, você pode lançar uma exceção se os valores não estiverem no enum: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") ou pode usá-lo ao chamar este método: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth
Seu método tem complexidade linear O (n). Melhor usar a pesquisa em HashMap predefinido com complexidade O (1).
Eldar Agalarov
sim, eu sei, mas na maioria dos casos, o enum terá um número muito pequeno de estados, então não importa de qualquer maneira, o que é mais legível.
Humazed
27

Não faz muito sentido neste caso, mas aqui está uma "extração lógica" para a solução do @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

Em geral, é isso que os objetos companheiros podem reutilizar (ao contrário dos membros estáticos em uma classe Java)

voddan
fonte
Por que você usa aula aberta? Basta torná-lo abstrato.
Eldar Agalarov
21

Outra opção, que poderia ser considerada mais "idiomática", seria a seguinte:

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

Que pode então ser usado como Type[type].

Ivan Plantevin
fonte
Definitivamente mais idiomático! Felicidades.
AleksandrH
6

Eu me vi fazendo a pesquisa reversa por valor personalizado, codificado à mão algumas vezes e descobri a seguinte abordagem.

Faça com que enumimplementem uma interface compartilhada:

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
}

Essa interface (por mais estranho que seja o nome :)) marca um certo valor como código explícito. O objetivo é ser capaz de escrever:

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

O que pode ser facilmente alcançado com o seguinte código:

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
fonte
3
É muito trabalho para uma operação tão simples, a resposta aceita é muito mais clara IMO
Connor Wyatt
2
Concordo totalmente para um uso simples, é definitivamente melhor. Eu já tinha o código acima para lidar com nomes explícitos para um determinado membro enumerado.
miensol
Seu código está usando reflexão (ruim) e está inchado (ruim também).
Eldar Agalarov
1

Uma variante de algumas propostas anteriores pode ser a seguinte, usando o campo ordinal e 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)
    }
}

}

incisões
fonte
1

Outro exemplo de implementação. Isso também define o valor padrão (aqui para OPEN) se a entrada não corresponder a nenhuma opção enum:

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

Veio com uma solução mais genérica

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

Exemplo de uso:

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

Verdadeira maneira idiomática de Kotlin. Sem código de reflexão inchado:

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

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

:)

shmulik.r
fonte
Isso funciona para constantes 0, 1, ..., N. Se você as tiver como 100, 50, 35, então não dará um resultado correto.
CoolMind