Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to obtain all subclasses of a given sealed class?

Tags:

kotlin

Recently we upgraded one of our enum class to sealed class with objects as sub-classes so we can make another tier of abstraction to simplify code. However we can no longer get all possible subclasses through Enum.values() function, which is bad because we heavily rely on that functionality. Is there a way to retrieve such information with reflection or any other tool?

PS: Adding them to a array manually is unacceptable. There are currently 45 of them, and there are plans to add more.


This is how our sealed class looks like:

sealed class State

object StateA: State()
object StateB: State()
object StateC: State()
....// 42 more

If there is an values collection, it will be in this shape:

val VALUES = setOf(StateA, StateB, StateC, StateC, StateD, StateE,
    StateF, StateG, StateH, StateI, StateJ, StateK, StateL, ......

Naturally no one wants to maintain such a monster.

like image 950
glee8e Avatar asked Jun 27 '17 15:06

glee8e


People also ask

Can we inherit sealed class?

Sealed classes are used to restrict the inheritance feature of object oriented programming. Once a class is defined as a sealed class, the class cannot be inherited.

Can sealed classes be base class?

A sealed class cannot be used as a base class. For this reason, it cannot also be an abstract class. Sealed classes prevent derivation. Because they can never be used as a base class, some run-time optimizations can make calling sealed class members slightly faster.

Can sealed classes be data?

What are sealed classes? Sealed classes represent a restricted class hierarchy. This allows you to define subclasses within the scope of the parent function, allowing you to represent hierarchies. In this case, the child or subclass can be of any type, a data class, an object, a regular class, or another sealed class.


3 Answers

In Kotlin 1.3+ you can use sealedSubclasses.

In prior versions, if you nest the subclasses in your base class then you can use nestedClasses:

Base::class.nestedClasses

If you nest other classes within your base class then you'll need to add filtering. e.g.:

Base::class.nestedClasses.filter { it.isFinal && it.isSubclassOf(Base::class) }

Note that this gives you the subclasses and not the instances of those subclasses (unlike Enum.values()).


With your particular example, if all of your nested classes in State are your object states then you can use the following to get all of the instances (like Enum.values()):

State::class.nestedClasses.map { it.objectInstance as State }

And if you want to get really fancy you can even extend Enum<E: Enum<E>> and create your own class hierarchy from it to your concrete objects using reflection. e.g.:

sealed class State(name: String, ordinal: Int) : Enum<State>(name, ordinal) {
    companion object {
        @JvmStatic private val map = State::class.nestedClasses
                .filter { klass -> klass.isSubclassOf(State::class) }
                .map { klass -> klass.objectInstance }
                .filterIsInstance<State>()
                .associateBy { value -> value.name }

        @JvmStatic fun valueOf(value: String) = requireNotNull(map[value]) {
            "No enum constant ${State::class.java.name}.$value"
        }

        @JvmStatic fun values() = map.values.toTypedArray()
    }

    abstract class VanillaState(name: String, ordinal: Int) : State(name, ordinal)
    abstract class ChocolateState(name: String, ordinal: Int) : State(name, ordinal)

    object StateA : VanillaState("StateA", 0)
    object StateB : VanillaState("StateB", 1)
    object StateC : ChocolateState("StateC", 2)
}

This makes it so that you can call the following just like with any other Enum:

State.valueOf("StateB")
State.values()
enumValueOf<State>("StateC")
enumValues<State>()

UPDATE

Extending Enum directly is no longer supported in Kotlin. See Disallow to explicitly extend Enum class : KT-7773.

like image 178
mfulton26 Avatar answered Oct 14 '22 06:10

mfulton26


With Kotlin 1.3+ you can use reflection to list all sealed sub-classes without having to use nested classes: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/sealed-subclasses.html

I asked for some feature to achieve the same without reflection: https://discuss.kotlinlang.org/t/list-of-sealed-class-objects/10087

like image 8
Romain F. Avatar answered Oct 14 '22 06:10

Romain F.


Full example:

sealed class State{
    companion object {
        fun find(state: State) =
            State::class.sealedSubclasses
                    .map { it.objectInstance as State}
                    .firstOrNull { it == state }
                    .let {
                        when (it) {
                            null -> UNKNOWN
                            else -> it
                        }
                    }
    }
    object StateA: State()
    object StateB: State()
    object StateC: State()
    object UNKNOWN: State()

}
like image 5
Naruto Sempai Avatar answered Oct 14 '22 05:10

Naruto Sempai