kotlingenericsenums

Use generic type to get associate type in enum by raw value


This is an Enum class:

enum class Foo(rawValue: Int) {
    FIRST(1),
    SECOND(2)
}

I can use companion object to retrieve the type by raw value:

enum class Foo(rawValue: Int) {
    FIRST(1),
    SECOND(2)


   companion object {
    
      fun getCaseBy(value: Int): Foo? {
        return entries.firstOrNull { it.rawValue == value }
      }
   }
}

Then use it like: Foo.getCaseBy(1)

Any way using generic type to put this getCaseBy into interface? Then there's no need to add such method for every enum?

Something like:

interface EnumInterface<T> {
    
    companion object {
        
        fun getCaseBy(value: E): T? {
            return T.entries.firstOrNull { it.rawValue == value }
        }
    }
}

enum class Foo(rawValue: Int): EnumInterface(Foo::class) {
    FIRST(1),
    SECOND(2)
}
  1. T is restricted to Enum class, E is retrieve T's rawValue type.
  2. Only Enum type can confirm this interface EnumInterface

Is this possible?


Solution

  • This is a cleaner implementation inspired by Louis Wasserman's comment.

    import kotlin.reflect.KClass
    
    abstract class EnumGetter<E : Enum<E>, V>(private val enumClass: KClass<E>) {
    
      abstract fun predicate(e: E, value: V): Boolean;
    
      fun getEnum(value: V): E? {
        return enumClass.java.enumConstants.firstOrNull { predicate(it, value) }
      }
    }
    
    enum class Foo(val rawValue: Int) {
      FIRST(1),
      SECOND(2);
    
      companion object : EnumGetter<Foo, Int>(Foo::class) {
    
        override fun predicate(e: Foo, value: Int): Boolean {
          return e.rawValue == value
        }
      }
    }
    
    fun main() {
      println(Foo.getEnum(1))
      println(Foo.getEnum(2))
      println(Foo.getEnum(3))
    }
    

    If you think overriding predicate function is too troublesome, you can apply the same method as in the old version below to create an annotation for the key field.


    This is the old version.

    You can implement this via reflection.

    import kotlin.reflect.full.hasAnnotation
    import kotlin.reflect.full.memberProperties
    
    @Target(AnnotationTarget.PROPERTY)
    @Retention(AnnotationRetention.RUNTIME)
    annotation class Key
    
    inline fun <reified E : Enum<E>> getValue(enumObject: E): Any? {
      val field = E::class.memberProperties.first { it.hasAnnotation<Key>() }
      return field.get(enumObject)
    }
    
    inline fun <reified E : Enum<E>> getValueBy(value: Any?): E {
      return enumValues<E>().first { getValue<E>(it) == value }
    }
    
    enum class Foo(@Key val rawValue: Int) {
      FIRST(1),
      SECOND(2);
    }
    
    fun main() {
      println(getValueBy<Foo>(2));
    }
    

    Since enum class can have various property names, the annotation Key is used to mark the property used for comparing.

    Note that if no property is annotated, getValue will throw a NoSuchElement error.