sealed class 与使用关联类型时的枚举

sealed class vs enum when using associated type

我想根据 Int 创建一个颜色对象。我可以使用 sealed classenum 获得相同的结果,并且想知道一个是否比另一个更好。

使用sealed class:

sealed class SealedColor(val value: Int) {
    class Red : SealedColor(0)
    class Green : SealedColor(1)
    class Blue : SealedColor(2)

    companion object {
        val map = hashMapOf(
            0 to Red(),
            1 to Green(),
            2 to Blue()
        )
    }
}

val sealedColor: SealedColor = SealedColor.map[0]!!
when (sealedColor) {
                is SealedColor.Red -> print("Red value ${sealedColor.value}")
                is SealedColor.Green -> print("Green value ${sealedColor.value}")
                is SealedColor.Blue -> print("Blue value ${sealedColor.value}")
            }

使用enum:

enum class EnumColor(val value: Int) {
    Red(0),
    Green(1),
    Blue(2);

    companion object {
        fun valueOf(value: Int): EnumColor {
            return EnumColor
                .values()
                .firstOrNull { it.value == value }
                    ?: throw NotFoundException("Could not find EnumColor with value: $value")
        }
    }
}

val enumColor: EnumColor = EnumColor.valueOf(0)
when (enumColor) {
            EnumColor.Red -> print("Red value ${enumColor.value}")
            EnumColor.Green -> print("Green value ${enumColor.value}")
            EnumColor.Blue -> print("Blue value ${enumColor.value}")
        }

它们在性能方面是否相同?是否有更好的 kotlin 方法来实现相同的结果?

A sealed class 是 “枚举 classes 的扩展”。它们可以存在于包含状态的多个实例中,而每个枚举常量仅作为单个实例存在。

因为在您的示例中,您不需要多次实例化值并且它们不提供特殊行为,所以 enums 应该适合使用案例.

另请参阅 docs

让我们通过对比示例从各个方面讨论枚举和密封 classes 之间的区别。这将帮助您根据您的用例选择一个而不是另一个。


属性

枚举

在枚举classes中,每个枚举值不能有自己唯一的属性。您被迫对每个枚举值具有相同的 属性:

enum class DeliveryStatus(val trackingId: String?) {
    PREPARING(null),
    DISPATCHED("27211"),
    DELIVERED("27211"),
}

这里我们只需要 trackingId 用于 DISPATCHEDDELIVEREDPREPARING 被强制具有 null 值。

密封Class

在密封 classes 的情况下,我们可以为每个子类型设置不同的属性:

sealed class DeliveryStatus
class Preparing() : DeliveryStatus()
class Dispatched(val trackingId: String) : DeliveryStatus()
class Delivered(val trackingId: String, val receiversName: String) : DeliveryStatus()

在这里我们为每个子类型设置了不同的属性。 Preparing 不需要我们的用例的属性,因此我们可以灵活地不指定任何 属性 ,这与枚举中的强制 null 值不同。 Dispatched 有一个 属性 而 Delivered 有两个属性。

考虑到问题中的示例 Color(val value: Int),您对所有常量有一个共同的 value: Int 属性,并且由于您不需要不同常量的不同属性,您应该使用在这种情况下枚举。


函数

枚举

枚举可以有抽象函数也可以有常规函数。但是像属性一样,每个枚举值也必须具有相同的功能:

enum class DeliveryStatus {
    PREPARING {
        override fun cancelOrder() = println("Cancelled successfully")
    },
    DISPATCHED {
        override fun cancelOrder() = println("Delivery rejected")
    },
    DELIVERED {
        override fun cancelOrder() = println("Return initiated")
    };

    abstract fun cancelOrder()
}

在这个例子中,我们有一个 abstract 函数 cancelOrder(),我们必须在每个枚举值中 override。这意味着,我们不能为不同的枚举值使用不同的函数。

用法:

class DeliveryManager {
    fun cancelOrder(status: DeliveryStatus) {
        status.cancelOrder()
    }
}

密封Class

在密封的 classes 中,我们可以为不同的子类型提供不同的功能:

sealed class DeliveryStatus

class Preparing : DeliveryStatus() {
    fun cancelOrder() = println("Cancelled successfully")
}

class Dispatched : DeliveryStatus() {
    fun rejectDelivery() = println("Delivery rejected")
}

class Delivered : DeliveryStatus() {
    fun returnItem() = println("Return initiated")
}

这里我们有不同的功能:cancelOrder()代表PreparingrejectDelivery()代表DispatchedreturnItem()代表Delivered。这使意图更清晰并使代码更具可读性,我们也可以选择不使用该功能,以防我们不想使用。

用法:

class DeliveryManager {
    fun cancelOrder(status: DeliveryStatus) = when(status) {
        is Preparing -> status.cancelOrder()
        is Dispatched -> status.rejectDelivery()
        is Delivered -> status.returnItem()
    }
}

如果我们想要像枚举示例中的所有子类型的通用函数,我们可以通过在 sealed class 本身中定义它然后在子类型中覆盖它来在 sealed class 中拥有它:

sealed class DeliveryStatus {
    abstract fun cancelOrder()
}

所有类型都有一个通用函数的好处是我们不必使用 is 运算符进行类型检查。我们可以简单地使用多态性,如 DeliveryManager class of enum example.


继承

枚举

由于enum值是对象,它们不能被扩展:

class LocallyDispatched : DeliveryStatus.DISPATCHED { }    // Error

enum class 是隐含的 final,所以它不能被其他 class 扩展:

class FoodDeliveryStatus : DeliveryStatus() { }            // Error

枚举classes不能扩展其他classes,它们只能扩展接口:

open class OrderStatus { }
interface Cancellable { }

enum class DeliveryStatus : OrderStatus() { }              // Error
enum class DeliveryStatus : Cancellable { }                // OK

密封Class

由于sealed class'子类型是类型,所以可以扩展:

class LocallyDispatched : Dispatched() { }                 // OK

密封的class本身当然可以扩展!:

class PaymentReceived : DeliveryStatus()                   // OK

密封 classes 可以扩展其他 classes 以及接口:

open class OrderStatus { }
interface Cancellable { }

sealed class DeliveryStatus : OrderStatus() { }           // OK
sealed class DeliveryStatus : Cancellable { }             // OK

实例数

枚举

由于枚举值是对象而不是类型,我们不能创建它们的多个实例:

enum class DeliveryStatus(val trackingId: String?) {
    PREPARING(null),
    DISPATCHED("27211"),
    DELIVERED("27211"),
}

在这个例子中,DISPATCHED是一个对象而不是类型,所以它只能作为一个实例存在,我们不能从它创建更多的实例:

// Single instance
val dispatched1 = DeliveryStatus.DISPATCHED               // OK

// Another instance
val dispatched2 = DeliveryStatus.DISPATCHED("45234")      // Error

密封Class

sealed classes 的子类型是类型,因此我们可以创建这些类型的多个实例。我们还可以使用 object 声明使一个类型只有一个实例:

sealed class DeliveryStatus
object Preparing : DeliveryStatus()
class Dispatched(val trackingId: String) : DeliveryStatus()
data class Delivered(val receiversName: String) : DeliveryStatus()

在这个例子中,我们可以创建多个 DispatchedDelivered 的实例。请注意,我们已经利用了 sealed class 的子类型的能力作为单例 object、常规 classdata classPreparing 可以只有一个 object,就像一个枚举值:

// Multiple Instances
val dispatched1 = Dispatched("27211")                     // OK
val dispatched2 = Dispatched("45234")                     // OK

// Single Instance
val preparing1 = Preparing                                // OK
val preparing2 = Preparing()                              // Error

还要注意,在上面的代码中,Dispatched 的每个实例对于 trackingId 属性.

可以有不同的值

可序列化和可比较

枚举

Kotlin 中的每个 enum class 都由抽象 class java.lang.Enum 隐式扩展。因此,所有枚举值自动具有 equals()toString()hashCode()SerializableComparable 的实现。我们不必定义它们。

密封Class

对于密封的classes我们需要手动定义它们或者使用data class自动equals(),toString()hashcode()然后实现SerializableComparable 手动。


性能

枚举

枚举不会被垃圾回收,它们会在应用的整个生命周期内保留在内存中。这可能是好的,也可能是坏的。

垃圾收集过程很昂贵。对象的创建也是如此,我们不想一次又一次地创建相同的对象。因此,使用枚举,您可以节省垃圾收集和对象创建的成本。这是好处。

缺点是即使不使用,枚举也会保留在内存中,这样可以一直占用内存。

如果您的应用程序中有 100 到 200 个枚举,则无需担心所有这些。但是当你有更多的时候,你可以根据枚举的数量、它们是否会一直被使用以及分配给你的 JVM 的内存量等事实来决定是否应该使用枚举。

枚举值的比较在 when 表达式中更快,因为在幕后,它使用 tableswitch 来比较对象。因此,对于问题中给出的示例,应该首选枚举,因为在这种情况下它们会更快。

在Android中,启用优化后,Proguard会将没有函数和属性的枚举转换为整数,因此您在编译时获得枚举的类型安全性,并且运行时整数的性能!

密封Class

Sealed classes 只是常规的 classes,唯一的例外是它们需要在同一个包和同一个编译单元中扩展。因此,它们的性能相当于常规 classes.

密封 classes 子类型的对象像常规 classes 的对象一样被垃圾收集。因此,您必须承担垃圾收集和对象创建的成本。

当您的内存限制较低时,如果您需要数千个对象,您可以考虑使用密封的 classes 而不是枚举。因为垃圾收集器可以在对象不使用时释放内存。

如果您使用 object 声明来扩展密封的 class,对象将作为单例,它们不会被垃圾收集,就像枚举一样。

密封 class' 类型的比较在 when 表达式中较慢,因为在幕后它使用 instanceof 来比较类型。在这种情况下,枚举和密封 classes 之间的速度差异非常小。仅当您在循环中比较数千个常量时才重要。


就是这样!希望这能让您更轻松地做出选择。