-
[Kotlin new features] Sealed interface, Data object, Enum entries프로그래밍/Kotlin 2023. 9. 16. 21:03반응형
Sealed class, Sealed interface 정리
- sealed class : abstract class 처럼 하나의 클래스만 상속가능
- sealed interface : 일반 interface처럼 여러 개 상속 가능
- Kotlin 1.5 버전 아래부터 sealed class의 sub-class는 같은 class에 있어야 했음
- Kotlin 1.5 버전 이후 부터는 같은 class 까지는 아니고 동일 package 있으면 됨
- 다른 모듈에서 참조가 가능한가?
- interface, abstract class : 다른 모듈에서도 참조 가능.
- sealed class/interface : 다른 모듈에서 참조 불가능. 동일 package에 있어야함
sealed class sealed interface abstract interface Multiple inheritance X O X O Sub-class inherits from everywhere X
same packageX
same packageO
any moduleO
any moduleSealed class
seal 봉인하다 sealed 봉인된
- Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance.
- They are useful when we have a very strict inheritance hierarchy, with a specific set of possible subclasses and no others. The compiler guarantees that only classes defined in the same source file as the sealed class are able to inherit from it. As of Kotlin 1.5, sealed classes can now have subclasses in all files of the same compilation unit and the same package.
- Third-party clients can't extend your sealed class in their code.
- In some sense, sealed classes are similar to enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances, each with its own state.
Sealed class vs abstract class
A sealed class is abstract by itself, it cannot be instantiated directly and can have abstract members. It is similar to java abstract class.
- Sealed, abstract classes can be derived from each other and form inheritance hierarchies.
abstract class AbstractClass1 abstract class AbstractClass2 sealed class SealedClass1 sealed class SealedClass2 // single inheritance class ExtendedClass : AbstractClass1() // O class ExtendedClass : SealedClass1() // O // multiple inheritance class ExtendedClass : AbstractClass1(), AbstractClass2() // X class ExtendedClass : SealedClass1(), SealedClass2() // X
- It can't make a subclass part of multiple hierarchies.
// module com.example.lib abstract class AbstractClass sealed class SealedClass // pacakage com.example.lib class Extended : AbstractClass() // O class Extended : SealedClass() // O // pacakage com.example.lib.a // sealed calss allows inheritance in the same package class Extended : AbstractClass() // O class Extended : SealedClass() // X // module com.example.app // sealed calss allows inheritance in the same package class Extended : AbstractClass() // O class Extended : SealedClass() // X
Sealed interface
- Sealed interfaces were introduced in Kotlin 1.5.
- Sealed interface is the fact that we can make a subclass part of multiple sealed hierarchies like java interface.
- The same works for sealed interfaces and their implementations: once a module with a sealed interface is compiled, no new implementations can appear.
- Third-party clients can't extend your sealed class in their code.
Sealed interface vs interface
- It can make a subclass part of multiple hierarchies.
interface Interface1 interface Interface2 sealed interface SealedInterface1 sealed interface SealedInterface2 // single inheritance class ExtendedClass1 : Interface1 // O class ExtendedClass2 : SealedInterface1 // O // multiple inheritance class ExtendedClass3 : Interface1, Interface2 // O class ExtendedClass4 : SealedInterface1, SealedInterface2 // O
- Once a module with a sealed interface is compiled, no new implementations can appear.
// module com.example.lib interface Interface1 sealed interface SealedInterface1 // pacakage com.example.lib class Extended : Interface1 // O class Extended : SealedInterface1 // O // pacakage com.example.lib.a // sealed interface allows inheritance in the same package class Extended : Interface1 // O class Extended : SealedInterface1 // X // module com.example.app // sealed interface allows inheritance in the same package class Extended : Interface1 // O class Extended : SealedInterface1 // X
Sealed class vs sealed interface
If we want to provide a bird with some navigation instructions. Let’s introduce a sealed class `Direction` for that with four cases:
sealed class Direction object Up : Direction() object Down : Direction() object Left : Direction() object Right : Direction() fun move(direction: Direction) = when(direction) { Up -> flyHigher() Down -> flyLower() Left -> turnLeft() Right -> turnRight() }
Suddenly, QuickBird’s left wing starts hurting very badly because of yesterday’s COVID vaccination. He needs to land and continue on foot. So from now on, we can only provide him with horizontal navigation instructions: he can either go left or right, but no longer up or down.
Let’s introduce two new sealed classes for this purpose: HorizontalDirection and VerticalDirection, with the two respective cases. Of course, we don’t want to abandon our general Direction sealed class. We hope for the best and assume that QuickBird will be able to fly again tomorrow! 💪
Here, we hit a limitation of sealed classes: A sealed class is a special form of an abstract class and subclasses can only inherit from a single abstract class. Thus, the following code will not compile:sealed class Direction sealed class HorizontalDirection sealed class VerticalDirection object Up : Direction(), VerticalDirection() ⚡️ object Down : Direction(), VerticalDirection() ⚡️ object Left : Direction(), HorizontalDirection() ⚡️ object Right : Direction(), HorizontalDirection() ⚡️
To overcome this limitation for general abstract classes we would use interfaces. For sealed classes, we now have sealed interfaces for this purpose – and suddenly each direction object can have multiple conformances:
sealed interface Direction sealed interface HorizontalDirection sealed interface VerticalDirection object Up : Direction, VerticalDirection object Down : Direction, VerticalDirection object Left : Direction, HorizontalDirection object Right : Direction, HorizontalDirection fun move(direction: HorizontalDirection) = when(direction) { Left -> turnLeft() Right -> turnRight() }
Data object
- It just landed in with the Kotlin version 1.7.20 as an experimental feature.
- It is currently planned to be released in version 1.9.
Data object example
- Below we have a typical example of a sealed class hierarchy where we are using a sealed interface (we could also use a sealed class) to define possible states for UI.
- We are using a data class for the success state and an object for error and loading states since we don’t need any additional information.
sealed interface UiState { data class Success(val username: String): ProfileScreenState object Error: ProfileScreenState object Loading: ProfileScreenState }
- What if we want to log/print the current screen state for either debugging reasons or for sending it to an analytics service? The string representation of the ProfileScreenState.Success data class contains the name of the class and all its properties, which is exactly what we want.
- But if we print a plain object declaration in Kotlin, we’ll get a string containing the full package name, object name, and the address of where this object is stored in memory. And since objects are singletons in Kotlin, the address part will remain the same each time we print that object and is not relevant to us.
Success(username=exampleUser1) com.example.sealedclass.ProfileScreenState$Error@4d1bf319 com.example.sealedclass.ProfileScreenState$Loading@5c80cf32
One solution would be to override the toString(): String function on each object and provide our implementation, but that seems like a lot of boilerplate code for such a trivial issue.
sealed interface ProfileScreenState { data class Success(val username: String) : ProfileScreenState object Error : ProfileScreenState { override fun toString(): String = "Error" } object Loading : ProfileScreenState { override fun toString(): String = "Loading" } }
Kotlin is planning to solve this using data objects. A data object is identical to a regular object but provides a default implementation of the toString() function that will print its name, removing the need to override it manually, and aligning behavior with a data class definition. They are especially useful for sealed class hierarchies to match behavior with data classes.
sealed interface ProfileScreenState { data class Success(val username: String) : ProfileScreenState data object Error : ProfileScreenState data object Loading : ProfileScreenState }
And if we print them now, we can see that their string representation now looks similar to a data class and only the object name is printed.
Success(username=exampleUser1) Error Loading
Enum entries
Enum classes in Kotlin have synthetic methods for listing the defined enum constants and getting an enum constant by its name. The signatures of these methods are as follows (assuming the name of the enum class is EnumClass):
EnumClass.values(): Array<EnumClass>
Below is an example of these methods in action:
enum class RGB { RED, GREEN, BLUE } fun main() { for (color in RGB.values()) println(color.toString()) // prints RED, GREEN, BLUE }
In 1.8.20, the entries property for enum classes was introduced as an Experimental feature. The entries property is a modern and performant replacement for the synthetic values() function. In 1.9.0, the entries property is Stable.
The entries property returns a pre-allocated immutable list of your enum constants. This is particularly useful when you are working with collections and can help you avoid performance issues.
- Decommission of Enum.values() with the IDE assistance without deprecation.
- To avoid that, values() will be softly decommissioned with the help of IDE assistance:
- values will be de-prioritized and eventually removed from IDE auto-completion.
- Soft warning with an intention to replace call to values() with call to entries will be introduced.
- Starting with IntelliJ IDEA 2023.1, if you have opted in to this feature, the appropriate IDE inspection will notify you about converting from values() to entries and offer a quick-fix.
- To avoid that, values() will be softly decommissioned with the help of IDE assistance:
- Introduce property entries: EnumEntries<E> that returns an unmodifiable list of all enum entries.
- Effectively, entries represents an immutable list of all enum entries and can be represented as type List<E>.
enum class RGB { RED, GREEN, BLUE } fun main() { for (color in RGB.entries) println(color.toString()) // prints RED, GREEN, BLUE }
Arrays are mutable by default, meaning that each call to values() always has to allocate a new instance of the array.
enum MyEnum extends Enum<MyEnum> { private static final synthetic MyEnum[] $VALUES private static final synthetic EnumEntries<MyEnum> $ENTRIES; <clinit> { A = new MyEnum("A", 0); $VALUES = $values(); $ENTRIES = EnumEntriesKt.enumEntries($VALUES); // internal factory from standard library } public static MyEnum[] values() { return $VALUES.clone(); } public static EnumEntries<MyEnum> getEntries() { return $ENTRIES; } private synthetic static MyEnum[] $values() { return new MyEnum[] { A }; } } // EnumEntries.kt public sealed interface EnumEntries<E : Enum<E>> : List<E>
Refers
https://github.com/Kotlin/KEEP/blob/master/proposals/enum-entries.md#examples-of-performance-issues
https://kotlinlang.org/docs/enum-classes.html#working-with-enum-constants
https://quickbirdstudios.com/blog/sealed-interfaces-kotlin/
https://quickbirdstudios.com/blog/sealed-interfaces-kotlin/
https://medium.com/hongbeomi-dev/sealed-class%EC%99%80-sealed-interface-db1fff634860
https://medium.com/@domen.lanisnik/data-objects-in-kotlin-1a549bfad657
https://medium.com/hongbeomi-dev/sealed-class%EC%99%80-sealed-interface-db1fff634860
반응형'프로그래밍 > Kotlin' 카테고리의 다른 글
Testing Kotlin coroutines on Android (0) 2022.07.19 Coroutine exceptions handling (0) 2022.06.22 Coroutine - Cancellation and timeouts (0) 2022.05.09 Making our Android Studio Apps Reactive with UI Components & Redux (0) 2020.11.08 Kotlin multiplatform 프로젝트를 생성해보자 (0) 2020.09.01