J'essaie de trouver le meilleur moyen de faire une "recherche inversée" sur une enum de Kotlin. Une de mes recettes de Effective Java était d’introduire une carte statique à l’intérieur de l’énum pour gérer la recherche inversée. Porter ceci sur Kotlin avec une simple énumération me conduit à un code ressemblant à ceci:
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]
}
}
}
Ma question est la suivante: est-ce la meilleure façon de procéder ou existe-t-il une meilleure? Et si j'ai plusieurs enums qui suivent un modèle similaire? Y a-t-il un moyen dans Kotlin de rendre ce code plus réutilisable dans tous les enums?
Tout d’abord, l’argument de fromInt()
devrait être un Int
, pas un Int?
. Essayer d'obtenir un Type
en utilisant null mènera évidemment à null, et un appelant ne devrait même pas essayer de le faire. Le Map
n'a également aucune raison d'être mutable. Le code peut être réduit à:
companion object {
private val map = Type.values().associateBy(Type::value)
fun fromInt(type: Int) = map[type]
}
Ce code est si court que, franchement, je ne suis pas sûr que cela vaut la peine d'essayer de trouver une solution réutilisable.
Cela n'a pas beaucoup de sens dans ce cas, mais voici une "extraction logique" pour la solution de @ 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
En général, les objets compagnons peuvent être réutilisés (contrairement aux membres statiques d'une classe Java)).
nous pouvons utiliser find
qui renvoie le premier élément correspondant au prédicat donné, ou null si aucun élément de ce type n'a été trouvé.
companion object {
fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}
Je me suis retrouvé à faire la recherche inversée par la coutume, codé à la main, valeur plusieurs fois et est venu avec l'approche suivante.
Assurez-vous que enum
s implémente une interface partagée:
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
}
Cette interface (aussi étrange que le nom soit:)) marque une certaine valeur comme code explicite. Le but est de pouvoir écrire:
val a = Alphabet::class.decode(1) //Alphabet.A
val d = Alphabet::class.tryDecode(4) //null
Ce qui peut être facilement réalisé avec le code suivant:
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)
Une autre option, qui pourrait être considérée comme plus "idiomatique", serait la suivante:
companion object {
private val map = Type.values().associateBy(Type::value)
operator fun get(value: Int) = map[value]
}
Ce qui peut alors être utilisé comme Type[type]
.
Un autre exemple de mise en œuvre. Ceci définit également la valeur par défaut (ici à OPEN
) si aucune entrée ne correspond à aucune option 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
}
}
Une variante de certaines propositions précédentes pourrait être la suivante, en utilisant un champ ordinal et 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)
}
}
}