value class în Kotlin

Clase și obiecte Kotlin 1.5

value class învelește o singură valoare într-un tip nou, pentru siguranță la compilare, fără costul unui obiect la rulare.

Sintaxă
@JvmInline
value class Nume(val valoare: Tip) {
    init { require(...) }
    fun metoda() = ...
}

Ce face value class

Dacă o funcție primește (idClient: Long, idComanda: Long), poți inversa ușor argumentele fără nicio eroare. Cu value class creezi tipuri separate, IdClient și IdComanda, iar compilatorul te oprește când le amesteci. La rulare, de cele mai multe ori, obiectul dispare și rămâne doar valoarea din interior, deci nu plătești o alocare în plus.

O value class are exact o proprietate val în constructorul primar. Poate avea blocuri init (pentru validare), metode și proprietăți calculate, poate implementa interfețe, dar nu poate avea alte câmpuri cu stare. Pe JVM se scrie cu adnotarea @JvmInline.

Egalitatea == compară valoarea din interior; === nu este permis. Când este pusă într-o colecție generică sau folosită ca tip nullable, valoarea este „împachetată” într-un obiect, ca un Int într-o List<Int>.

Exemple

Exemplul 1: Tipuri separate pentru identificatori

trimite cere întâi un IdClient, apoi un IdComanda. Inversarea lor ar da eroare de compilare, deși amândouă țin un Long.

@JvmInline
value class IdClient(val valoare: Long)

@JvmInline
value class IdComanda(val valoare: Long)

fun trimite(client: IdClient, comanda: IdComanda) =
    println("Comanda ${comanda.valoare} pleacă spre clientul ${client.valoare}")

fun main() {
    val client = IdClient(1507)
    val comanda = IdComanda(98_001)
    trimite(client, comanda)
    // trimite(comanda, client)   // eroare: argument type mismatch
    println(client == IdClient(1507))
}

Rezultat

Comanda 98001 pleacă spre clientul 1507
true

Exemplul 2: Validare și metode

init refuză sumele negative. Lei are operatorul plus și o afișare proprie.

@JvmInline
value class Lei(val bani: Long) {
    init { require(bani >= 0) { "Suma nu poate fi negativă: $bani" } }

    operator fun plus(alt: Lei) = Lei(bani + alt.bani)
    override fun toString() = "%d,%02d lei".format(bani / 100, bani % 100)
}

fun main() {
    val total = Lei(1250) + Lei(799)
    println("Total: $total")
    try {
        Lei(-5)
    } catch (e: IllegalArgumentException) {
        println("Eroare: ${e.message}")
    }
}

Rezultat

Total: 20,49 lei
Eroare: Suma nu poate fi negativă: -5

Exemplul 3: Email validat

Un Email poate fi creat doar cu un text care conține @; restul codului știe că adresa a fost verificată.

@JvmInline
value class Email(val adresa: String) {
    init { require("@" in adresa) { "Adresă invalidă: $adresa" } }
    val domeniu get() = adresa.substringAfter("@")
}

fun creeazaEmail(text: String): Email? = runCatching { Email(text.trim().lowercase()) }.getOrNull()

fun main() {
    for (text in listOf("Ana.Pop@Exemplu.ro", "fara-arond", " mihai@iasi.ro ")) {
        val e = creeazaEmail(text)
        println(if (e == null) "Respins: '$text'" else "Acceptat: ${e.adresa} (domeniu ${e.domeniu})")
    }
}

Rezultat

Acceptat: ana.pop@exemplu.ro (domeniu exemplu.ro)
Respins: 'fara-arond'
Acceptat: mihai@iasi.ro (domeniu iasi.ro)

Greșeli frecvente

  • Uitarea lui @JvmInline pe JVM: eroare value classes without '@JvmInline' annotation are not yet supported.
  • Două proprietăți în constructor (value class Punct(val x: Int, val y: Int)): nu este permis; folosește data class.
  • Folosirea lui === pe valori value class: nu are sens, pentru că nu au identitate; folosește ==.

Comenzi înrudite

  • data class: data class este o clasă pentru date: primește automat toString, equals, hashCode, copy și componentN.
  • typealias: Dă un nume nou, mai scurt sau mai clar, unui tip existent; nu creează un tip nou.
  • class: class declară o clasă: un tip nou care grupează date (proprietăți) și comportament (metode).
  • operator: operator marchează o funcție cu nume special (plus, get, contains, compareTo…) ca să poată fi folosită prin +, [], in sau <.