Закрытый класс против перечисления при использовании связанного типа
Я хотел бы создать цветной объект на основе 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 для достижения того же результата?
3 ответа
Давайте обсудим разницу между перечислениями и запечатанными классами в различных аспектах с помощью контрастных примеров. Это поможет вам выбрать один из вариантов в зависимости от вашего варианта использования.
Характеристики
Enum
В классах перечисления каждое значение перечисления не может иметь собственное уникальное свойство. Вы вынуждены иметь одно и то же свойство для каждого значения перечисления:
Здесь нам нужны только для и
DELIVERED
, то
PREPARING
вынужден иметь ценность.
Запечатанный класс
В случае запечатанных классов у нас могут быть разные свойства для каждого подтипа:
sealed class DeliveryStatus
class Preparing() : DeliveryStatus()
class Dispatched(val trackingId: String) : DeliveryStatus()
class Delivered(val trackingId: String, val receiversName: String) : DeliveryStatus()
Здесь у нас есть разные свойства для каждого подтипа. не нужны свойства для нашего варианта использования, поэтому у нас есть возможность не указывать какое-либо свойство, в отличие от принудительного
null
значения в перечислениях. имеет одно свойство, а у него два свойства.
Рассматривая пример
Color(val value: Int)
в вопросе у вас есть общий
value: Int
свойство для всех констант, и поскольку вам не нужны разные свойства для разных констант, в этом случае следует использовать перечисления.
Функции
Enum
Перечисления могут иметь абстрактные функции, а также обычные функции. Но, как и свойства, каждое значение перечисления также должно иметь одинаковую функцию:
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
функция, которую мы должны
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()
за ,
rejectDelivery()
для и
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
класс примера перечисления.
Наследование
Enum
С
enum
значения являются объектами, они не могут быть расширены:
class LocallyDispatched : DeliveryStatus.DISPATCHED { } // Error
Неявно
final
, поэтому он не может быть расширен другими классами:
class FoodDeliveryStatus : DeliveryStatus() { } // Error
Классы Enum не могут расширять другие классы, они могут расширять только интерфейсы:
open class OrderStatus { }
interface Cancellable { }
enum class DeliveryStatus : OrderStatus() { } // Error
enum class DeliveryStatus : Cancellable { } // OK
Запечатанный класс
Поскольку подтипы закрытых классов являются типами, они могут быть расширены:
class LocallyDispatched : Dispatched() { } // OK
Сам класс sealed, конечно, можно расширить !:
class PaymentReceived : DeliveryStatus() // OK
Запечатанные классы могут расширять другие классы, а также интерфейсы:
open class OrderStatus { }
interface Cancellable { }
sealed class DeliveryStatus : OrderStatus() { } // OK
sealed class DeliveryStatus : Cancellable { } // OK
Количество экземпляров
Enum
Поскольку значения перечисления являются объектами, а не типами, мы не можем создавать их несколько экземпляров:
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
Запечатанный класс
Подтипы запечатанных классов являются типами, поэтому мы можем создать несколько экземпляров этих типов. Мы также можем сделать тип имеющим только один экземпляр, используя объявление:
sealed class DeliveryStatus
object Preparing : DeliveryStatus()
class Dispatched(val trackingId: String) : DeliveryStatus()
data class Delivered(val receiversName: String) : DeliveryStatus()
В этом примере мы можем создать несколько экземпляров и
Delivered
. Обратите внимание, что мы использовали способность подтипов запечатанного класса быть как одиночными, так и обычными.
class
или а.
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
свойство.
Сериализуемый и сопоставимый
Enum
Каждый
enum class
в Kotlin неявно расширяется абстрактным классом
java.lang.Enum
. Итак, все значения перечисления автоматически имеют реализации для,,
hashCode()
, и . Нам не нужно их определять.
Запечатанный класс
Для запечатанных классов нам нужно определить их вручную или использовать
data class
для автоматического
equals()
,
toString()
и
hashcode()
а затем реализовать
Serializable
и
Comparable
вручную.
Спектакль
Enum
Перечисления не собирают мусор, они остаются в памяти на время жизни вашего приложения. Это может быть как положительная, так и отрицательная сторона.
Процесс сборки мусора стоит дорого. То же верно и для создания объектов, мы не хотим создавать одни и те же объекты снова и снова. Таким образом, с перечислениями вы экономите затраты на сборку мусора, а также на создание объекта. Это положительный момент.
Обратной стороной является то, что перечисления остаются в памяти, даже когда они не используются, это может постоянно держать память занятой.
Вам не нужно обо всем этом беспокоиться, если в вашем приложении от 100 до 200 перечислений. Но когда у вас их больше, вы должны решить, следует ли вам переходить на перечисления, в зависимости от таких фактов, как количество перечислений, будут ли они использоваться все время и объем памяти, выделенный вашей JVM.
Сравнение значений перечисления выполняется быстрее в выражении, потому что под капотом оно использует
tableswitch
для сравнения объектов. Итак, для примера, приведенного в вопросе, следует отдавать предпочтение перечислениям, поскольку в этом случае они будут быстрее.
В Android, когда включена оптимизация, Proguard преобразует перечисления, не имеющие функций и свойств, в целые числа, поэтому вы получаете безопасность типов перечислений во время компиляции и производительность целых чисел во время выполнения!
Запечатанный класс
Запечатанные классы - это обычные классы с единственным исключением, что они должны быть расширены в том же пакете и в той же единице компиляции. Итак, их производительность эквивалентна обычным занятиям.
Объекты подтипов запечатанных классов собираются сборщиком мусора, как и объекты обычных классов. Таким образом, вы должны нести расходы на сборку мусора, а также на создание объекта.
Если у вас нехватка памяти, вы можете рассмотреть возможность использования запечатанных классов вместо перечислений, если вам нужны тысячи объектов. Потому что сборщик мусора может освободить память, когда объекты не используются.
Если вы используете
object
объявление для расширения запечатанного класса, объекты действуют как одиночные объекты, и они не будут собираться сборщиком мусора, как и перечисления.
Сравнение типов запечатанных классов происходит медленнее в
when
выражение, потому что под капотом он использует
instanceof
для сравнения типов. Однако разница в скорости между перечислениями и запечатанными классами в этом случае очень небольшая. Это имеет значение только тогда, когда вы сравниваете тысячи констант в цикле.
Вот и все! Надеюсь, вам будет проще выбрать одно из них.
sealed
классы называются "расширением перечислимых классов". Они могут существовать в нескольких экземплярах, которые содержат состояние. Поскольку вам не нужно, чтобы значения создавались несколько раз, и они не обеспечивают особого поведения, перечисления должны быть просто подходящими для варианта использования и более простыми для понимания здесь.
Там просто нет необходимости sealed
в вашем примере.
Мое решение такое, и оно даст вам список всех экземпляров подклассаval list = BaseClass::class.sealedSubclasses.map{it.createInstance()}