Proprietăți delegate (by lazy, observable, map) în Kotlin

Clase și obiecte Kotlin 1.0

O proprietate delegată (val x by ...) lasă un alt obiect să decidă ce se întâmplă la citire și scriere: lazy, observable, map.

Sintaxă
val x: Tip by lazy { calcul }
var y: Tip by Delegates.observable(initial) { prop, vechi, nou -> ... }
val z: Tip by mapa                         // citește din Map după numele proprietății

operator fun getValue(thisRef: Any?, property: KProperty<*>): Tip

Ce face Proprietăți delegate (by lazy, observable, map)

Cu by, citirea și scrierea unei proprietăți sunt trimise unui obiect delegat. Biblioteca standard are delegați gata făcuți: lazy { } calculează valoarea la prima citire și o păstrează; Delegates.observable(initial) { ... } rulează codul tău după fiecare schimbare; Delegates.vetoable poate refuza o schimbare; un Map poate fi delegat, iar proprietatea citește valoarea de la cheia cu același nume.

Poți scrie și propriul delegat: o clasă cu operator fun getValue(thisRef, property) (și setValue pentru var). property.name îți dă numele proprietății. Delegații sunt utili când același comportament (validare, jurnalizare, salvare) se repetă la multe proprietăți.

by lazy este sigur pentru mai multe fire în mod implicit: valoarea se calculează o singură dată chiar dacă mai multe fire o citesc deodată.

Exemple

Exemplul 1: by lazy: calcul la prima citire

Mesajul „calculez” apare o singură dată, la prima citire, nu la crearea obiectului.

class Raport(private val vanzari: List<Int>) {
    val total: Int by lazy {
        println("(calculez totalul...)")
        vanzari.sum()
    }
}

fun main() {
    val r = Raport(listOf(120, 340, 95))
    println("Raport creat")
    println("Total: ${r.total} lei")
    println("Din nou: ${r.total} lei")
}

Rezultat

Raport creat
(calculez totalul...)
Total: 555 lei
Din nou: 555 lei

Exemplul 2: observable și vetoable

observable afișează fiecare schimbare. vetoable returnează false pentru o valoare negativă, deci schimbarea nu are loc.

import kotlin.properties.Delegates

class Comanda {
    var stare: String by Delegates.observable("plasată") { _, vechi, nou ->
        println("Starea: $vechi -> $nou")
    }

    var cantitate: Int by Delegates.vetoable(1) { _, _, nou -> nou > 0 }
}

fun main() {
    val c = Comanda()
    c.stare = "expediată"
    c.stare = "livrată"

    c.cantitate = 5
    c.cantitate = -2
    println("Cantitate: ${c.cantitate}")
}

Rezultat

Starea: plasată -> expediată
Starea: expediată -> livrată
Cantitate: 5

Exemplul 3: Delegare către Map și delegat propriu

nume și oras se citesc din map după numele lor. Majuscule este un delegat propriu care transformă orice text primit.

import kotlin.reflect.KProperty

class Majuscule {
    private var valoare = ""
    operator fun getValue(thisRef: Any?, property: KProperty<*>) = valoare
    operator fun setValue(thisRef: Any?, property: KProperty<*>, nou: String) {
        println("Setez ${property.name} = $nou")
        valoare = nou.uppercase()
    }
}

class Client(date: Map<String, Any?>) {
    val nume: String by date
    val oras: String by date
    var cod: String by Majuscule()
}

fun main() {
    val c = Client(mapOf("nume" to "Elena Ionescu", "oras" to "Sibiu"))
    c.cod = "ro-sb-17"
    println("${c.nume} din ${c.oras}, cod ${c.cod}")
}

Rezultat

Setez cod = ro-sb-17
Elena Ionescu din Sibiu, cod RO-SB-17

Greșeli frecvente

  • var x by lazy { }: lazy merge doar cu val.
  • Delegare către un Map fără cheia respectivă: citirea aruncă NoSuchElementException (Key oras is missing in the map).
  • Lipsa lui import kotlin.properties.Delegates: Delegates nu este importat automat.

Comenzi înrudite

  • lazy: Creează o valoare calculată abia la prima folosire, apoi păstrată; se folosește cu val nume by lazy { … }.
  • by (delegare): by trimite implementarea unei interfețe către alt obiect (delegare de clasă) sau citirea unei proprietăți către un delegat.
  • get și set (getteri și setteri): get și set definesc ce se întâmplă când o proprietate este citită sau modificată; field este valoarea păstrată în spate.
  • lateinit: lateinit permite unei proprietăți var ne-nullable să primească valoare mai târziu, după crearea obiectului.
  • operator: operator marchează o funcție cu nume special (plus, get, contains, compareTo…) ca să poată fi folosită prin +, [], in sau <.