Kotlin中枚举与密封类全方位对比

3,637 阅读6分钟

前言

Kotlin中枚举和密封类功能有重复,该怎么选择?下文通过对两者多角度对比,让你彻底理清关系。本文翻译自stack overflow,原文点这里

一、特性

枚举

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

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

这里只有DISPATCHEDDELIVERED需要trackingId这个属性,但是PREPARING被强制有一个null值。

密封类

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

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

在这里,每个子类型都有不同的属性。在我们的例子中,Preparing不需要属性,因此我们可以灵活地不指定任何属性。Dispatched有一个属性,Delivered有两个属性。

二、函数

枚举

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

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(),我们必须在每个枚举值中重写它。这意味着,我们不能为不同的枚举值使用不同的函数。

用法:

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

这里我们有不同的函数: Preparing中的cancelOrder()Dispatched中的rejectDelivery()Delivered中的returnItem()。这使意图更清晰并使代码更具可读性,当然如果我们不想要,也可以选择不使用函数。

用法:

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枚举示例的类所示。

三、继承

枚举

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

class LocallyDispatched : DeliveryStatus.DISPATCHED { }    // Error

枚举类默认是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创建多个实例。请注意,我们利用了密封类的子类型作为单例、常规类或者一个数据类型的能力。Preparing 只能有一个对象,就像枚举值一样:

// 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 中每个枚举类都由抽象类java.lang.Enum隐式继承。因此,所有的枚举值自动拥有了实现equals()toString()hashCode()SerializableComparable。我们不必定义它们。

密封类

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

六、性能表现

枚举

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

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

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

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

when表达式中枚举值的比较更快,因为在幕后,它使用tableswitch比较对象。

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

密封类

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

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

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

如果您使用object声明来继承密封类,则对象将充当单例,像枚举一样不会被垃圾回收。

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