48

我想创建一个基于Int. 我可以使用sealed classand获得相同的结果,enum并且想知道一个是否比另一个更好。

使用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 方法来实现相同的结果?

4

2 回答 2

94

让我们通过对比示例讨论枚举和密封类在各个方面的区别。这将帮助您根据您的用例选择一个。


特性

枚举

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

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

在这里,我们需要trackingId唯一的DISPATCHEDand DELIVERED,而 thePREPARING被强制有一个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()for PreparingrejectDelivery()forDispatchedreturnItem()for 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()

Dispatched在此示例中,我们可以创建和的多个实例Delivered。请注意,我们利用了密封类的子类型作为单例object、常规class或 a 的能力data classPreparingcan 只能有一个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 中的Everyenum class被抽象类隐式扩展java.lang.Enum。因此,所有枚举值都自动具有equals()toString()hashCode()Serializable的实现Comparable。我们不必定义它们。

密封类

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


表现

枚举

枚举不会被垃圾收集,它们会在您的应用程序的生命周期内保留在内存中。这可能是有利的,也可能是不利的。

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

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

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

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

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

密封类

密封类只是普通类,唯一的例外是它们需要在同一个包和同一个编译单元中扩展。所以,他们的表现相当于普通班。

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

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

如果您使用object声明来扩展密封类,则对象充当单例并且它们不会被垃圾收集,就像枚举一样。

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


而已!希望这将使您更容易选择其中一个。

于 2020-12-09T23:14:51.023 回答
28

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

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

另外,请参阅文档

于 2018-03-08T09:08:18.287 回答