Classi e data class
Finora ogni dato era una variabile separata: un nome di qua, un voto di là. Una classe ti permette di descrivere un "tipo di cosa" e di creare tante copie di quella cosa, ognuna con i suoi valori. In Kotlin, per i dati esiste una scorciatoia speciale: la data class.
- dichiarare una
data classcon le sue proprietà; - creare oggetti e leggere o modificare le proprietà (
val/var); - il
toString()e il confronto==automatici; copy()per creare una copia cambiando solo alcuni valori;- aggiungere metodi propri della classe;
- la destrutturazione
val (x, y) = punto.
Ripasso lampo: stringhe ed elenchi
Le classi servono anche a mettere ordine nei testi: ripasso del capitolo 8. E prima di avere gli oggetti, gli elenchi di cose si tenevano con liste e calcoli: ripasso del capitolo 6.
Che cosa stampa questo programma?
fun main() {
val s = "programmare"
println(s.substring(0, 7))
println(s[0])
}program e pEsatto: substring(0, 7) prende dall'indice 0 (compreso) al 7 (escluso), e s[0] è il primo carattere.programm e pIl secondo indice è escluso: si ferma al carattere 6, quindi il risultato è di 7 caratteri.rogramma e pQuesto sarebbe substring(1, 8): il primo numero è l'inizio, non la lunghezza.substring(a, b) prende i caratteri da a (compreso) a b (escluso): la lunghezza del pezzo è la differenza fra i due indici, non il secondo indice.
Che cosa stampa questo programma?
fun main() {
val prezzi = mutableListOf(5, 12, 8, 20)
val cari = prezzi.filter { it > 10 }
println(cari.size)
println(cari.sum())
}2 e 32Esatto: filter tiene solo 12 e 20, quindi due elementi, e sum() li somma in 32.4 e 45Questo è il risultato senza filtro: size e sum() su tutti e quattro i prezzi.2 e 2020 è solo l'ultimo prezzo che supera 10: sum() somma tutti gli elementi della lista filtrata.filter restituisce una nuova lista, e size e sum() lavorano su quella. Con un elenco di valori, però, il prezzo non dice a che cosa appartiene: per tenere insieme un nome e i suoi numeri serve una piccola classe, come data class in questo capitolo.
9.1 Perché le classi: mettere insieme i dati
Immagina di dover gestire dei libri. Con le variabili di prima servirebbero tre variabili
per ogni libro: titolo, autore, prezzo. Con dieci libri diventa un caos.
Una data class descrive una volta sola che forma ha un libro, poi crei
quanti libri vuoi.
println(p)usa iltoString()che Kotlin scrive per te:Punto(x=3, y=4), con i nomi delle proprietà.copy(y = 10)crea un nuovo oggetto cambiando soloy:Punto(x=3, y=10). L'oggettopnon viene toccato.p == Punto(3, 4)confronta i valori, non l'indirizzo: ètrue.- La destrutturazione
val (a, b) = qmette3inae10inb.
data class Libro(val titolo: String, val autore: String, var prezzo: Double)
si legge così: creo un tipo chiamato Libro i cui oggetti hanno un
titolo, un autore e un prezzo.
I parametri scritti con val diventano proprietà non modificabili,
quelli scritti con var (come prezzo) si possono cambiare dopo la creazione.
9.2 toString() e == arrivano gratis
Nelle data class Kotlin scrive per te due cose importantissime:
toString(), che trasforma l'oggetto in testo leggibile, e ==,
che confronta il contenuto dei due oggetti (non "sono lo stesso oggetto in memoria?"
ma "hanno gli stessi valori?").
- Con
data classKotlin scrive per te un==che confronta i valori delle proprietà: i dueD(1)sono uguali, quinditrue. - Con una
classnormale quel confronto non c'è:==controlla se i due oggetti sono lo stesso oggetto in memoria, e qui sono due oggetti diversi... - ...quindi
C(1) == C(1)èfalse, anche se i valori sono identici.
Usa data class quando l'oggetto serve soprattutto a tenere dei dati
(uno studente, un prodotto, una prenotazione): ottieni toString, ==,
copy e la destrutturazione senza scrivere nulla.
Usa class semplice quando l'oggetto ha un comportamento con stato che cambia nel tempo
(un contatore, una connessione). Ne vedrai un esempio più avanti.
9.3 copy(): una copia cambiando solo qualcosa
copy(...) crea un nuovo oggetto con gli stessi valori, tranne quelli
che indichi fra parentesi. È comodissimo quando vuoi "quasi lo stesso oggetto" con una differenza.
La proprietà prezzo è dichiarata val nel costruttore. Vuoi uno stesso libro con un prezzo diverso. Che cosa fai?
libro.prezzo = 15.0Kotlin ti ferma: una proprietà val non si può modificare dopo la creazione.val nuovo = libro.copy(prezzo = 15.0)Esatto: copy crea un nuovo oggetto con tutti i valori uguali tranne quelli che indichi.copy fa proprio questo lavoro.Regola: var nel costruttore = proprietà modificabile; val = per cambiarla serve copy.
val nuovo = vecchio.copy(voto = 10) non tocca vecchio:
sono due oggetti distinti. Serve anche perché le proprietà val
non si possono modificare: invece di cambiarle, crei una copia aggiornata.
9.4 I metodi: far fare qualcosa all'oggetto
Dentro la classe puoi scrivere funzioni: diventano metodi che usi con
nomeOggetto.nomeMetodo(). Dentro al metodo puoi leggere le proprietà scrivendone
semplicemente il nome.
fun area(): Int = base * altezza è
fun area(): Int { return base * altezza } scritti in modo più corto.
Per calcoli di una riga si usa il =; per più istruzioni si usano le graffe con return.
9.5 Proprietà nel corpo, init e toString personalizzato
Oltre ai parametri del costruttore, puoi dichiarare altre proprietà nel corpo della classe
e inizializzarle con un blocco init { }.
In una class normale, poi, puoi decidere tu cosa restituisce
toString() scrivendo override fun toString().
Ti serve un tipo che tiene dei dati (uno studente con nome e voto) e vuoi che il confronto con == e la stampa funzionino bene senza scrivere codice. Che cosa dichiari?
data classEsatto: ottieni gratis toString(), == per contenuto, copy() e la destrutturazione.class normaleCon una class normale devi scrivere tu toString() e non hai né == per contenuto né copy().array di due valoriFunzionerebbe, ma perdi i nomi dei campi: studente[0] non dice niente, studente.nome sì.Usa data class quando l'oggetto è soprattutto un contenitore di dati; una class normale quando ha un comportamento con stato che cambia nel tempo (per esempio un contatore).
Una class normale non ha == per contenuto né
copy() né la destrutturazione: le ottieni solo con data class.
Per tenere dati usa quindi quasi sempre data class.
9.6 Destrutturare una data class
I valori del costruttore si possono "spacchettare" in variabili separate.
È comodo per lavorare con i valori evitando di scrivere ogni volta punto.x.
9.7 Riepilogo
| Scrivi | Cosa fa |
|---|---|
data class C(val a: Int, var b: String) | dichiara un tipo con due proprietà (una fissa, una modificabile) |
val o = C(1, "x") | crea un oggetto (chiama il costruttore) |
o.a, o.b | legge una proprietà |
o.b = "y" | cambia una proprietà dichiarata con var |
o.toString() | testo leggibile, automatico nelle data class: C(a=1, b=x) |
o == p | true se tutte le proprietà sono uguali (data class) |
o.copy(a = 5) | nuovo oggetto con a cambiato e il resto uguale |
val (x, y) = punto | estrapola le proprietà in variabili |
fun nome(): Tipo { ... } | metodo della classe, si chiama con o.nome() |
fun metodo(param: Tipo = valore) | argomento con valore di default |
init { ... } | codice eseguito alla creazione dell'oggetto |
override fun toString() | personalizza il testo di un oggetto (class normale) |
9.8 Esercizi guidati
Leggi tre dati: nome di un prodotto, prezzo unitario e quantità.
Crea una data class Prodotto(val nome: String, val prezzo: Double, val quantita: Int)
con un metodo totale() che restituisce prezzo * quantita.
Stampa una scheda con nome, prezzo, quantità e totale (con due decimali).
Il metodo si scrive fun totale(): Double = prezzo * quantita.
Per i due decimali usa "%.2f".format(...).
- La
data classdichiara per prima la "forma" del prodotto: le tre proprietà arrivano dal costruttore conval. Prima il tipo, poi l'uso. fun totale(): Double = prezzo * quantitaè un metodo: dentro vede le proprietà come se fossero variabili normali.scheda()mette insieme testo e numeri usando il template$nomee la formattazione"%.2f".format(...).- Dopo aver descritto il tipo, il programma legge i tre dati (
readln()per il nome,readlnOrNull()?.toDoubleOrNull() ?: 0.0per il prezzo) e costruisce l'oggetto conProdotto(nome, prezzo, quantita).
Crea una data class Studente(val nome: String, val voto: Int) e tre studenti
("Anna" 8, "Luca" 5, "Sara" 9). Stampa la pagella, la media dei voti (due decimali),
il nome del migliore e infine il migliore "con lode" creato con copy(voto = 10).
Per il migliore parti da var migliore = classe[0] e aggiornalo quando
s.voto > migliore.voto. La copia con la lode è migliore.copy(voto = 10).
- La lista
listOf(...)contiene tre oggettiStudentecreati con il costruttore. - Nel ciclo
foraccumuliamo la somma dei voti e cerchiamo il voto più alto, come si fa con i numeri normali:s.votoè unInt. - La media si calcola con
somma.toDouble() / classe.size: iltoDouble()evita la divisione intera. migliore.copy(voto = 10)crea un nuovo oggetto con lo stesso nome e voto cambiato: l'oggettomiglioreoriginale resta invariato.- Stampando l'oggetto con
"... " + migliore.copy(voto = 10)entra in gioco iltoString()automatico:Studente(nome=Sara, voto=10).
Crea una data class Spettatore(val nome: String, val eta: Int) con un metodo
prezzo(): 5.0 euro sotto i 14 anni, 6.0 euro dai 65 in su, 9.0 euro altrimenti.
Leggi nome ed età, stampa il biglietto e poi, con copy, il prezzo
che pagherebbe la stessa persona se avesse 70 anni.
Il prezzo può essere una if usata come espressione:
fun prezzo(): Double = if (eta < 14) 5.0 else if (eta >= 65) 6.0 else 9.0.
prezzo()usa la forma= if (...) ... else ...: in Kotlin unaifpuò essere un'espressione che restituisce un valore.- Le condizioni si controllano in ordine: prima i minori di 14 anni, poi i maggiori di 64; chi non rientra paga il prezzo pieno.
biglietto()chiama un altro metodo della stessa classe scrivendo semplicementeprezzo(): i metodi si vedono tra loro.copy(eta = 70)ecopy(eta = 10)mostrano in modo semplice come cambierebbe il prezzo: è utile per provare casi diversi senza riscrivere l'oggetto.
Scrivi tu il programma, senza codice di partenza. Crea una
data class Riga(val nome: String, val quantita: Int, val prezzo: Double)
con un metodo totale() che restituisce quantita * prezzo.
Leggi tre articoli di magazzino, uno dopo l'altro: per ognuno il nome,
la quantità e il prezzo unitario, sempre in quest'ordine
(tre righe di input per articolo). Metti gli articoli in una lista e stampa: l'elenco con
il totale di ogni riga (due decimali), il totale del magazzino (due decimali) e la riga
più cara, cioè quella con il totale maggiore, con il suo nome.
Attenzione a un caso limite: una quantità può mancare (al posto del numero c'è una riga
vuota). Il programma non deve fermarsi: readlnOrNull()?.toIntOrNull() ?: 0
la considera 0.
Con i dati Quaderno 3 2.5, Zaino 1 39.9 e
Penne (vuoto) 0.8 deve stampare Totale: 47.40 euro e
Riga più cara: Zaino (39.90 euro).
La strategia: dichiara la classe con il suo metodo, poi riempi una
mutableListOf<Riga>() con un ciclo for (i in 1..3) che a ogni
giro legge le tre righe nello stesso ordine. Per il totale prepara un accumulatore
var totale = 0.0 e per la riga più cara tieni da parte l'intero
oggetto, non solo il numero: così alla fine hai anche il nome da stampare.
La quantità mancante non deve fermare il programma: toIntOrNull() ?: 0 la
trasforma in zero.
- La
data classdescrive una riga di magazzino etotale()è un metodo che calcola un valore derivato dalle proprietà: nome, quantità e prezzo viaggiano insieme, invece di essere liste separate da tenere allineate. - La lista si crea vuota con
mutableListOf<Riga>()e cresce dentro il ciclo conadd(...): i dati arrivano dall'input, quindi non si conoscono in anticipo. toIntOrNull() ?: 0gestisce il dato mancante: la conversione non va a buon fine e il?:fornisce il valore di riserva, così una riga vuota non ferma il programma.- Somma e ricerca del massimo sono gli algoritmi dei capitoli precedenti applicati a
r.totale(); tenere l'oggettopiuCarapermette di stamparne anche il nome, e la prima riga fa da valore iniziale del confronto.
Il programma deve descrivere un libro e produrre una copia scontata, ma non funziona. Deve stampare Libro(titolo=Kotlin, prezzo=20.0) e poi Libro(titolo=Kotlin, prezzo=15.0).
Una proprietà dichiarata val nel costruttore non si modifica: per avere lo stesso libro a un prezzo diverso si crea un oggetto nuovo con copy.
libro.prezzo = 15.0è un errore:prezzoè una proprietàval, quindi non si può cambiare dopo la creazione.copy(prezzo = 15.0)crea un nuovo oggetto con tutti i valori uguali tranne quello indicato.- L'oggetto di partenza resta
Libro(titolo=Kotlin, prezzo=20.0): questo è il vantaggio dicopy, che non tocca l'originale. - Se il prezzo dovesse cambiare davvero nel tempo, la proprietà andrebbe dichiarata
var:data class Libro(val titolo: String, var prezzo: Double).
data class con proprietà val e vardata classcopy() per creare una variante di un oggettoval (a, b) = oggetto