sealed class 与使用关联类型时的枚举
sealed class vs enum when using associated type
我想根据 Int
创建一个颜色对象。我可以使用 sealed class
和 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 方法来实现相同的结果?
A sealed
class 是 “枚举 classes 的扩展”。它们可以存在于包含状态的多个实例中,而每个枚举常量仅作为单个实例存在。
因为在您的示例中,您不需要多次实例化值并且它们不提供特殊行为,所以 enums 应该适合使用案例.
另请参阅 docs。
让我们通过对比示例从各个方面讨论枚举和密封 classes 之间的区别。这将帮助您根据您的用例选择一个而不是另一个。
属性
枚举
在枚举classes中,每个枚举值不能有自己唯一的属性。您被迫对每个枚举值具有相同的 属性:
enum class DeliveryStatus(val trackingId: String?) {
PREPARING(null),
DISPATCHED("27211"),
DELIVERED("27211"),
}
这里我们只需要 trackingId
用于 DISPATCHED
和 DELIVERED
,PREPARING
被强制具有 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()
代表Preparing
,rejectDelivery()
代表Dispatched
,returnItem()
代表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()
在这个例子中,我们可以创建多个 Dispatched
和 Delivered
的实例。请注意,我们已经利用了 sealed class 的子类型的能力作为单例 object
、常规 class
或 data 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
都由抽象 class java.lang.Enum
隐式扩展。因此,所有枚举值自动具有 equals()
、toString()
、hashCode()
、Serializable
和 Comparable
的实现。我们不必定义它们。
密封Class
对于密封的classes我们需要手动定义它们或者使用data class
自动equals()
,toString()
和hashcode()
然后实现Serializable
和 Comparable
手动。
性能
枚举
枚举不会被垃圾回收,它们会在应用的整个生命周期内保留在内存中。这可能是好的,也可能是坏的。
垃圾收集过程很昂贵。对象的创建也是如此,我们不想一次又一次地创建相同的对象。因此,使用枚举,您可以节省垃圾收集和对象创建的成本。这是好处。
缺点是即使不使用,枚举也会保留在内存中,这样可以一直占用内存。
如果您的应用程序中有 100 到 200 个枚举,则无需担心所有这些。但是当你有更多的时候,你可以根据枚举的数量、它们是否会一直被使用以及分配给你的 JVM 的内存量等事实来决定是否应该使用枚举。
枚举值的比较在 when
表达式中更快,因为在幕后,它使用 tableswitch
来比较对象。因此,对于问题中给出的示例,应该首选枚举,因为在这种情况下它们会更快。
在Android中,启用优化后,Proguard会将没有函数和属性的枚举转换为整数,因此您在编译时获得枚举的类型安全性,并且运行时整数的性能!
密封Class
Sealed classes 只是常规的 classes,唯一的例外是它们需要在同一个包和同一个编译单元中扩展。因此,它们的性能相当于常规 classes.
密封 classes 子类型的对象像常规 classes 的对象一样被垃圾收集。因此,您必须承担垃圾收集和对象创建的成本。
当您的内存限制较低时,如果您需要数千个对象,您可以考虑使用密封的 classes 而不是枚举。因为垃圾收集器可以在对象不使用时释放内存。
如果您使用 object
声明来扩展密封的 class,对象将作为单例,它们不会被垃圾收集,就像枚举一样。
密封 class' 类型的比较在 when
表达式中较慢,因为在幕后它使用 instanceof
来比较类型。在这种情况下,枚举和密封 classes 之间的速度差异非常小。仅当您在循环中比较数千个常量时才重要。
就是这样!希望这能让您更轻松地做出选择。
我想根据 Int
创建一个颜色对象。我可以使用 sealed class
和 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 方法来实现相同的结果?
A sealed
class 是 “枚举 classes 的扩展”。它们可以存在于包含状态的多个实例中,而每个枚举常量仅作为单个实例存在。
因为在您的示例中,您不需要多次实例化值并且它们不提供特殊行为,所以 enums 应该适合使用案例.
另请参阅 docs。
让我们通过对比示例从各个方面讨论枚举和密封 classes 之间的区别。这将帮助您根据您的用例选择一个而不是另一个。
属性
枚举
在枚举classes中,每个枚举值不能有自己唯一的属性。您被迫对每个枚举值具有相同的 属性:
enum class DeliveryStatus(val trackingId: String?) {
PREPARING(null),
DISPATCHED("27211"),
DELIVERED("27211"),
}
这里我们只需要 trackingId
用于 DISPATCHED
和 DELIVERED
,PREPARING
被强制具有 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()
代表Preparing
,rejectDelivery()
代表Dispatched
,returnItem()
代表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()
在这个例子中,我们可以创建多个 Dispatched
和 Delivered
的实例。请注意,我们已经利用了 sealed class 的子类型的能力作为单例 object
、常规 class
或 data 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
都由抽象 class java.lang.Enum
隐式扩展。因此,所有枚举值自动具有 equals()
、toString()
、hashCode()
、Serializable
和 Comparable
的实现。我们不必定义它们。
密封Class
对于密封的classes我们需要手动定义它们或者使用data class
自动equals()
,toString()
和hashcode()
然后实现Serializable
和 Comparable
手动。
性能
枚举
枚举不会被垃圾回收,它们会在应用的整个生命周期内保留在内存中。这可能是好的,也可能是坏的。
垃圾收集过程很昂贵。对象的创建也是如此,我们不想一次又一次地创建相同的对象。因此,使用枚举,您可以节省垃圾收集和对象创建的成本。这是好处。
缺点是即使不使用,枚举也会保留在内存中,这样可以一直占用内存。
如果您的应用程序中有 100 到 200 个枚举,则无需担心所有这些。但是当你有更多的时候,你可以根据枚举的数量、它们是否会一直被使用以及分配给你的 JVM 的内存量等事实来决定是否应该使用枚举。
枚举值的比较在 when
表达式中更快,因为在幕后,它使用 tableswitch
来比较对象。因此,对于问题中给出的示例,应该首选枚举,因为在这种情况下它们会更快。
在Android中,启用优化后,Proguard会将没有函数和属性的枚举转换为整数,因此您在编译时获得枚举的类型安全性,并且运行时整数的性能!
密封Class
Sealed classes 只是常规的 classes,唯一的例外是它们需要在同一个包和同一个编译单元中扩展。因此,它们的性能相当于常规 classes.
密封 classes 子类型的对象像常规 classes 的对象一样被垃圾收集。因此,您必须承担垃圾收集和对象创建的成本。
当您的内存限制较低时,如果您需要数千个对象,您可以考虑使用密封的 classes 而不是枚举。因为垃圾收集器可以在对象不使用时释放内存。
如果您使用 object
声明来扩展密封的 class,对象将作为单例,它们不会被垃圾收集,就像枚举一样。
密封 class' 类型的比较在 when
表达式中较慢,因为在幕后它使用 instanceof
来比较类型。在这种情况下,枚举和密封 classes 之间的速度差异非常小。仅当您在循环中比较数千个常量时才重要。
就是这样!希望这能让您更轻松地做出选择。