sealed class și sealed interface în Kotlin

Moștenire, interfețe și vizibilitate Kotlin 1.0 (sealed interface: Kotlin 1.5)

sealed declară o ierarhie închisă: toate subtipurile sunt cunoscute la compilare, deci when le poate verifica pe toate fără else.

Sintaxă
sealed interface Rezultat
data class Succes(val date: String) : Rezultat
data class Eroare(val mesaj: String) : Rezultat
data object Incarcare : Rezultat

when (r) { is Succes -> ...; is Eroare -> ...; Incarcare -> ... }   // fără else

Ce face sealed class și sealed interface

O clasă sau interfață sealed (sigilată) poate fi moștenită doar în același pachet și același modul în care a fost declarată. Compilatorul cunoaște deci toate subtipurile. Când faci when pe o valoare sealed și tratezi fiecare subtip, nu ai nevoie de else; dacă mai târziu adaugi un subtip, compilatorul arată toate when-urile care trebuie completate.

Este modul obișnuit în Kotlin de a modela stări sau rezultate care pot avea forme diferite: un răspuns de la server (succes cu date, eroare cu mesaj, încărcare), o plată (card, transfer, numerar), o comandă dintr-un meniu. Spre deosebire de enum, fiecare subtip poate avea alte proprietăți și pot exista mai multe obiecte din același subtip.

Subtipurile sunt de obicei data class (cu date) sau data object (fără date). O clasă sealed este abstractă și are constructorii protected sau private.

Exemple

Exemplul 1: Rezultatul unei operații

when tratează toate cele trei subtipuri, deci este exhaustiv fără else. Smart cast-ul face accesibile date și mesaj.

sealed interface Raspuns
data class Succes(val date: List<String>) : Raspuns
data class Eroare(val cod: Int, val mesaj: String) : Raspuns
data object Incarcare : Raspuns

fun afiseaza(r: Raspuns) = when (r) {
    is Succes -> "Am primit ${r.date.size} produse: ${r.date}"
    is Eroare -> "Eroare ${r.cod}: ${r.mesaj}"
    Incarcare -> "Se încarcă..."
}

fun main() {
    val raspunsuri = listOf(Incarcare, Succes(listOf("pâine", "lapte")), Eroare(404, "Magazinul nu există"))
    raspunsuri.forEach { println(afiseaza(it)) }
}

Rezultat

Se încarcă...
Am primit 2 produse: [pâine, lapte]
Eroare 404: Magazinul nu există

Exemplul 2: Clasă sealed cu proprietate comună

Toate plățile au suma (din clasa sealed), dar fiecare subtip are datele lui. comision calculează diferit pentru fiecare.

sealed class Plata(val suma: Double) {
    class Card(suma: Double, val banca: String) : Plata(suma)
    class Transfer(suma: Double, val iban: String) : Plata(suma)
    class Numerar(suma: Double) : Plata(suma)
}

fun comision(p: Plata): Double = when (p) {
    is Plata.Card -> if (p.banca == "BT") 0.0 else p.suma * 0.01
    is Plata.Transfer -> 2.5
    is Plata.Numerar -> 0.0
}

fun main() {
    val plati = listOf(Plata.Card(300.0, "ING"), Plata.Card(300.0, "BT"), Plata.Transfer(1200.0, "RO49AAAA1B31007593840000"), Plata.Numerar(50.0))
    for (p in plati) println("${p::class.simpleName} ${p.suma} lei -> comision ${comision(p)} lei")
}

Rezultat

Card 300.0 lei -> comision 3.0 lei
Card 300.0 lei -> comision 0.0 lei
Transfer 1200.0 lei -> comision 2.5 lei
Numerar 50.0 lei -> comision 0.0 lei

Exemplul 3: Stările unui semafor cu tranziții

Fiecare stare știe care este următoarea. Ierarhia închisă garantează că urmatoarea tratează toate cazurile.

sealed interface Semafor {
    data object Rosu : Semafor
    data object Galben : Semafor
    data class Verde(val secunde: Int) : Semafor
}

fun urmatoarea(s: Semafor): Semafor = when (s) {
    Semafor.Rosu -> Semafor.Verde(30)
    is Semafor.Verde -> Semafor.Galben
    Semafor.Galben -> Semafor.Rosu
}

fun main() {
    var s: Semafor = Semafor.Rosu
    repeat(5) {
        println(s)
        s = urmatoarea(s)
    }
}

Rezultat

Rosu
Verde(secunde=30)
Galben
Rosu
Verde(secunde=30)

Greșeli frecvente

  • Adăugarea unui else într-un when pe o clasă sealed: compilează, dar pierzi avertizarea când apare un subtip nou. Lasă when-ul fără else.
  • Încercarea de a moșteni o clasă sealed dintr-un alt modul sau pachet: nu este permis; toate subtipurile se declară lângă ea.
  • Folosirea lui sealed pentru valori simple fără date: un enum class este mai potrivit.

Comenzi înrudite

  • when: Alege o ramură după valoarea unei expresii sau după prima condiție adevărată; înlocuiește switch din alte limbaje.
  • is, !is și smart cast: is verifică tipul unei valori; după verificare, compilatorul o tratează automat ca acel tip (smart cast).
  • enum class: enum class declară un tip cu un număr fix de valori numite (zilele săptămânii, stările unei comenzi).
  • data class: data class este o clasă pentru date: primește automat toString, equals, hashCode, copy și componentN.
  • object: object declară o clasă cu un singur obiect (singleton) sau creează pe loc un obiect anonim care implementează o interfață.