ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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 package
    X
    same package
    O
    any module
    O
    any module

     

    Sealed 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.
    • 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

    반응형
Designed by Tistory.