使用关联类型时的密封类与枚举



我想创建一个基于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 方法来达到相同的结果?

让我们用对比的例子来讨论枚举和密封类在各个方面的区别。这将帮助您根据您的用例选择一个而不是另一个。

<小时 />

属性

枚举

在枚举类中,每个枚举值不能有其自己唯一的属性。强制每个枚举值具有相同的属性:

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

这里我们只需要DISPATCHEDDELIVEREDtrackingIdPREPARING被迫具有null值。

密封类

对于密封类,我们可以为每个子类型具有不同的属性:

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

密封类

在密封类中,我们可以对不同的子类型具有不同的函数:

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 DeliveryStatus {
abstract fun cancelOrder()
}

拥有适用于所有类型的通用函数的优点是我们不必使用is运算符进行类型检查。我们可以简单地使用多态性,如枚举的DeliveryManager类示例所示。

>继承枚举

由于enum值是对象,因此无法扩展它们:

class LocallyDispatched : DeliveryStatus.DISPATCHED { }    // Error

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

class FoodDeliveryStatus : DeliveryStatus() { }            // Error

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

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

密封类

由于密封类的子类型是类型,因此可以扩展它们:

class LocallyDispatched : Dispatched() { }                 // OK

当然,密封类本身可以扩展!

class PaymentReceived : DeliveryStatus()                   // OK

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

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

密封类

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

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

在此示例中,我们可以创建多个实例DispatchedDelivered.请注意,我们已经利用了密封类的子类型作为单例object、常规classdata class的能力。Preparing只能有一个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都由抽象类java.lang.Enum隐式扩展。因此,所有枚举值都会自动实现equals()toString()hashCode()SerializableComparable。我们不必定义它们。

密封类

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

<小时 />

性能

枚举

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

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

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

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

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

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

密封类

密封类只是常规类,唯一的例外是它们需要在同一包和同一编译单元中扩展。因此,它们的性能相当于常规课程。

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

当您具有低内存约束时,如果需要数千个对象,则可以考虑使用密封类而不是枚举。因为垃圾回收器可以在对象未使用时释放内存。

如果使用object声明来扩展密封类,则对象充当单例,并且不会像枚举一样对其进行垃圾回收。

在表达式when中,密封类类型的比较速度较慢,因为在引擎盖下它使用instanceof来比较类型。在这种情况下,枚举和密封类之间的速度差异很小。只有当您在循环中比较数千个常量时,它才重要。

<小时 />

就是这样!希望这将使您更容易选择一个而不是另一个。

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

由于在您的示例中,您不需要多次实例化这些值,并且它们不提供特殊行为,因此枚举应该适合用例。

另请参阅文档。

我的解决方案是这样的,它将为您提供所有子类实例的列表val list = BaseClass::class.sealedSubclasses.map{it.createInstance()}

最新更新