【问题标题】:How to obtain all subclasses of a given sealed class?如何获取给定密封类的所有子类?
【发布时间】:2017-06-27 15:32:03
【问题描述】:

最近我们将其中一个枚举类升级为密封类将对象作为子类,这样我们就可以创建另一层抽象来简化代码。但是,我们不能再通过Enum.values() 函数获得所有可能的子类,这很糟糕,因为我们严重依赖该功能。有没有办法通过反射或任何其他工具来检索这些信息?

PS:手动将它们添加到数组中是不可接受的。目前有 45 个,并且计划增加更多。


这就是我们的密封类的样子:

sealed class State

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

如果有一个 values 集合,它将是这个形状:

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

自然没有人愿意养这样一个怪物。

【问题讨论】:

  • AFAIK 手动将它们添加到数组中实际上是唯一可接受 的方法。您无法知道给定类的所有可能子类。这是 JVM 实际工作方式的限制。如果我错了,请纠正我。
  • 你想达到什么目的?可以发一些代码吗?
  • @m0skit0 实际上有 implementation 这样做,但是对于这种情况来说似乎太重了。
  • @D3xter 获取给定密封类的所有子类,如标题所述,用法是作为 Enum.values 的替换一些问题无需代码即可理解,这就是其中之一。如果你真的担心的话,我还是添加了一些代码。

标签: kotlin


【解决方案1】:

在 Kotlin 1.3+ 中,您可以使用 sealedSubclasses

在以前的版本中,如果您将子类嵌套在基类中,则可以使用nestedClasses

Base::class.nestedClasses

如果您在基类中嵌套其他类,则需要添加过滤。例如:

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

请注意,这会为您提供子类,而不是这些子类的实例(与 Enum.values() 不同)。


对于您的特定示例,如果您在 State 中的所有嵌套类都是您的 object 状态,那么您可以使用以下内容获取所有实例(如 Enum.values()):

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

如果你想变得更花哨,你甚至可以扩展Enum<E: Enum<E>> 并使用reflection 从它创建你自己的类层次结构到你的具体对象。例如:

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

这样您就可以像使用任何其他 Enum 一样调用以下命令:

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

更新

Kotlin 不再支持直接扩展 Enum。看 Disallow to explicitly extend Enum class : KT-7773.

【讨论】:

  • 好吧,在 KT-14657(感谢@HTNW)解决之前,我们应该接受这个。关于这个实例,好吧,我们设法发现KClass 有一个objectInstance 属性。
  • 对于那些像我一样无法立即弄清楚 isSubclassOf 来自哪里的人 - 它是 kotlin reflect api。将 compile "org.jetbrains.kotlin:kotlin-reflect:1.1.4-3" 添加到您的 build.gradle 依赖项
  • 感谢@Defuera 我已经添加说明,即类似枚举的解决方案需要反思。谢谢。
  • @HendraAnggrian,显然这在 Kotlin 1.1.4 中不再可行。 :-(
  • @mfulton26 感谢您的澄清。不过真可惜,这真是个巧妙的把戏。
【解决方案2】:

使用 Kotlin 1.3+,您可以使用反射列出所有密封的子类,而无需使用嵌套类:https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/sealed-subclasses.html

我要求一些功能来实现相同的功能而无需反射:https://discuss.kotlinlang.org/t/list-of-sealed-class-objects/10087

【讨论】:

  • 此功能很有用,但对于任何考虑使用此功能的人来说,重要的是要注意它可能非常慢,因为它使用反射。
【解决方案3】:

完整示例:

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()

}

【讨论】:

    【解决方案4】:

    明智的选择是在 kotlin 中使用 ServiceLoader。然后编写一些提供程序来获取一个通用的类、枚举、对象或数据类实例。例如:

    val provides = ServiceLoader.load(YourSealedClassProvider.class).iterator();
    
    val subInstances =  providers.flatMap{it.get()};
    
    fun YourSealedClassProvider.get():List<SealedClass>{/*todo*/};
    

    层次结构如下:

                    Provider                    SealedClass
                       ^                             ^
                       |                             |
                --------------                --------------
                |            |                |            |
            EnumProvider ObjectProvider    ObjectClass  EnumClass
                |            |-------------------^          ^
                |                    <uses>                 |
                |-------------------------------------------|
                                     <uses>
    

    另一个选项更复杂,但它可以满足您的需求,因为在同一个包中密封类。让我告诉你如何以这种方式归档:

    1. 获取密封类的 URL,例如:ClassLoader.getResource("com/xxx/app/YourSealedClass.class")
    2. 扫描所有密封类URL父级的jar条目/目录文件,例如:jar://**/com/xxx/appfile://**/com/xxx/app,然后找出所有"com/xxx/app/*.class"文件/条目。
    3. 使用ClassLoader.loadClass(eachClassName) 加载过滤的类
    4. 检查加载的类是否是你的密封类的子类
    5. 决定如何获取子类实例,例如:Enum.values()object.INSTANCE
    6. 返回已创建的密封类的所有实例

    【讨论】:

    • 这仍然意味着“将它们手动添加到数组中”,即使不是字面意思:)
    • @m0skit0 是的,先生。事实上,第一种方法是手动的。但它是出于维护目的。因为您可以将逻辑拆分为多个部分。
    • 是的,但问题指出“手动将它们添加到数组中是不可接受的”。
    • @m0skit0 是的,先生。然后我给他另一个复杂的选择。
    • 感谢您的复杂回答。这实际上是被考虑的,因为它提供了更好的灵活性,但我们认为这可能会对性能产生很大影响(扫描整个类路径听起来很糟糕)。我们宁愿让它们都嵌套类并使用 mfulton26 的解决方案。
    【解决方案5】:

    如果你想在孩子课堂上使用它,试试这个。

    open class BaseSealedClass(val value: String, val name: Int) {
        companion object {
            inline fun<reified T:BaseSealedClass> valueOf(value: String): T? {
                return T::class.nestedClasses
                    .filter { clazz -> clazz.isSubclassOf(T::class) }
                    .map { clazz -> clazz.objectInstance }
                    .filterIsInstance<T>()
                    .associateBy { it.value }[value]
            }
    
            inline fun<reified  T:BaseSealedClass> values():List<T> =
                T::class.nestedClasses
                .filter { clazz -> clazz.isSubclassOf(T::class) }
                .map { clazz -> clazz.objectInstance }
                .filterIsInstance<T>()
        }
    }
    
    @Stable
    sealed class Theme(value: String, name: Int): BaseSealedClass(value, name) {
        object Auto: Theme(value = "auto", name = R.string.setting_general_theme_auto)
        object Light: Theme(value= "light", name = R.string.setting_general_theme_light)
        object Dark: Theme(value= "dark", name = R.string.setting_general_theme_dark)
    
        companion object {
            fun valueOf(value: String): Theme? = BaseSealedClass.valueOf(value)
            fun values():List<Theme> = BaseSealedClass.values()
        }
    }
    
    
    

    【讨论】:

    • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
    【解决方案6】:

    对于没有反射的解决方案,这是一个支持在编译时为密封类生成类型列表的库: https://github.com/livefront/sealed-enum

    文档中的示例

    sealed class Alpha {
        object Beta : Alpha()
        object Gamma : Alpha()
        
        @GenSealedEnum
        companion object
    }
    

    将生成以下对象:

    object AlphaSealedEnum : SealedEnum<Alpha> {
        override val values: List<Alpha> = listOf(
            Alpha.Beta,
            Alpha.Gamma
        )
    
        override fun ordinalOf(obj: Alpha): Int = when (obj) {
            Alpha.Beta -> 0
            Alpha.Gamma -> 1
        }
    
        override fun nameOf(obj: AlphaSealedEnum): String = when (obj) {
            Alpha.Beta -> "Alpha_Beta"
            Alpha.Gamma -> "Alpha_Gamma"
        }
    
        override fun valueOf(name: String): AlphaSealedEnum = when (name) {
            "Alpha_Beta" -> Alpha.Beta
            "Alpha_Gamma" -> Alpha.Gamma
            else -> throw IllegalArgumentException("""No sealed enum constant $name""")
        }
      }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-17
      • 1970-01-01
      • 2023-04-01
      • 2016-06-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多