Generics () și where în Kotlin

Generics Kotlin 1.0

Generics permit clase și funcții care lucrează cu orice tip (T), verificat la compilare; where adaugă mai multe restricții pe T.

Sintaxă
class Cutie<T>(val continut: T)

fun <T> primul(lista: List<T>): T? = lista.firstOrNull()

fun <T : Comparable<T>> maxim(a: T, b: T): T

fun <T> f(x: T) where T : CharSequence, T : Comparable<T> { ... }

Ce face Generics (<T>) și where

Un tip generic are un parametru de tip, scris între < >: List<String>, Map<String, Int>. Și tu poți scrie clase (class Cutie<T>) și funcții (fun <T> primul(lista: List<T>)) generice. Compilatorul verifică tipurile: într-o Cutie<Int> nu poți pune un text, iar când scoți valoarea nu ai nevoie de conversie. De obicei tipul se deduce singur: Cutie(5) este Cutie<Int>.

O limită superioară restrânge tipurile acceptate: <T : Number> acceptă doar numere, deci în corp poți folosi toDouble(). Fără limită, T poate fi orice, inclusiv nullable (limita implicită este Any?); <T : Any> exclude null.

Când T trebuie să respecte mai multe condiții, le scrii cu where după semnătură: where T : CharSequence, T : Comparable<T>. Tipurile generice se șterg la rulare pe JVM; pentru is T ai nevoie de reified.

Exemple

Exemplul 1: Clasă și funcție generică

Cutie<T> păstrează o valoare de orice tip. ultimul merge pentru liste de orice tip și returnează T?.

class Cutie<T>(val continut: T) {
    fun descrie() = "Cutie cu: $continut"
}

fun <T> ultimul(lista: List<T>): T? = if (lista.isEmpty()) null else lista[lista.size - 1]

fun main() {
    val cutieText = Cutie("mere")
    val cutieNumar = Cutie(42)
    println(cutieText.descrie())
    println(cutieNumar.continut + 8)

    println(ultimul(listOf("Ana", "Mihai", "Ioana")))
    println(ultimul(listOf(3.5, 7.25)))
    println(ultimul(emptyList<Int>()))
}

Rezultat

Cutie cu: mere
50
Ioana
7.25
null

Exemplul 2: Limită superioară: și >

medie acceptă orice listă de numere. celMaiMare are nevoie ca elementele să poată fi comparate.

fun <T : Number> medie(valori: List<T>): Double = valori.sumOf { it.toDouble() } / valori.size

fun <T : Comparable<T>> celMaiMare(a: T, b: T): T = if (a >= b) a else b

fun main() {
    println(medie(listOf(9, 10, 8)))
    println(medie(listOf(2.5, 3.5)))
    println(celMaiMare(7, 12))
    println(celMaiMare("Iași", "Cluj"))
}

Rezultat

9.0
3.0
12
Iași

Exemplul 3: Mai multe restricții cu where

T trebuie să fie text (CharSequence) și comparabil. String le respectă pe amândouă.

fun <T> celMaiLungApoiAlfabetic(valori: List<T>): T?
        where T : CharSequence, T : Comparable<T> =
    valori.sortedWith(compareByDescending<T> { it.length }.thenBy { it }).firstOrNull()

fun main() {
    println(celMaiLungApoiAlfabetic(listOf("Arad", "Iași", "Brăila", "Galați")))
    println(celMaiLungApoiAlfabetic(listOf("Cluj", "Alba")))
    println(celMaiLungApoiAlfabetic(emptyList<String>()))
}

Rezultat

Brăila
Alba
null

Greșeli frecvente

  • x is T într-o funcție generică obișnuită: eroare cannot check for instance of erased type 'T'. Folosește inline + reified.
  • Presupunerea că T nu poate fi null: fără limită, T poate fi String?. Scrie <T : Any> dacă vrei să excluzi null.
  • Așteptarea ca List<Int> să poată fi folosită ca MutableList<Number>: vezi varianța (out/in).

Comenzi înrudite

  • out și in (varianță): out și in spun cum se moștenesc tipurile generice: out T doar produce valori (covariant), in T doar le consumă (contravariant).
  • reified: reified păstrează tipul generic T la rulare într-o funcție inline, ca să poți scrie x is T sau T::class.
  • typealias: Dă un nume nou, mai scurt sau mai clar, unui tip existent; nu creează un tip nou.
  • vararg: vararg permite unei funcții să primească oricâte argumente de același tip, văzute în corp ca un tablou.
  • class: class declară o clasă: un tip nou care grupează date (proprietăți) și comportament (metode).