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.
- che cos'è una
classe come si dichiara, con proprietà e metodi; - che differenza c'è fra
classedata 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 un metodo è una funzione che sta dentro una classe, come quelle del capitolo 9: qui ritrovi parametri, tipo di ritorno e return.
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.
10.1 Che cos'è una classe: descrivere un tipo di cosa
Finora ogni variabile conteneva un valore solo: per uno studente servivano due variabili
(nome e voto), per trenta studenti sessanta, e niente nel programma diceva
che nome1 e voto1 riguardano la stessa persona. Immagina di dover gestire
dei libri: con tre variabili per libro, dieci libri diventano trenta variabili da tenere allineate.
Una classe risolve proprio questo: descrive una volta sola che forma ha una
cosa — quali dati tiene e che cosa sa fare — e da quella descrizione crei poi quante copie vuoi,
ognuna con i suoi valori. La descrizione si chiama classe, le copie si chiamano
oggetti (o istanze). Si dichiara con la parola class:
class Studente(val nome: String, var voto: Int) si legge: creo un tipo chiamato
Studente i cui oggetti hanno un nome e un voto.
I parametri fra parentesi sono il costruttore: i valori che servono per creare
l'oggetto. Quelli scritti con val diventano proprietà non modificabili, quelli con
var si possono cambiare anche dopo la creazione.
La riga val anna = Studente("Anna", 7) crea un oggetto: chiama il
costruttore con i valori nell'ordine dei parametri. Da lì in poi anna.nome legge una
proprietà, luca.voto = 6 la cambia (solo se è var) e
anna.promosso() chiama un metodo, cioè una funzione che sta dentro
la classe e vede le proprietà come se fossero sue variabili. I metodi si scrivono come le funzioni
del capitolo 9, ma senza passare i dati come parametri: li trovano già lì.
Con nomi e voti in due liste, l'unica cosa che tiene insieme i dati
diventa l'ordine: voti[2] è il voto di nomi[2], e basta un
remove per far scivolare tutto di un posto. Con una classe il nome e il voto
viaggiano insieme, e studente.voto non si può sbagliare.
10.2 data class: la stessa idea, con meno da scrivere
La class dell'esempio 1 va benissimo così. Però i tipi che servono soprattutto a
tenere dei dati — un libro, uno studente, una prenotazione — sono la maggioranza dei
casi, e per loro Kotlin offre una scorciatoia: la data class. Si dichiara come una classe
normale, con data davanti, e in cambio Kotlin scrive per te quattro cose che altrimenti
dovresti scrivere a mano: toString(), il confronto ==, copy()
e la destrutturazione. Le vedrai tutte in questo capitolo.
Si legge esattamente come la classe dell'esempio 1: data class Libro(val titolo: String, val autore: String, var prezzo: Double)
vuol dire "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.
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.
10.3 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?"). La class dell'esempio 1 non le ha: se la stampi con
println non vedi i suoi valori ma il nome della classe con un codice dell'oggetto,
qualcosa come Studente@1 (prova a sostituire anna.nome con anna
nel laboratorio dell'esempio 1),
e il confronto == fra due oggetti con gli stessi valori dà false.
Qui sotto la differenza, misurata.
- 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): è il caso dell'esempio 1, dove conta il voto attuale dello
studente. Un esempio più completo arriva nel §10.6.
10.4 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.
10.5 I metodi: far fare qualcosa all'oggetto
Dentro la classe puoi scrivere funzioni: diventano metodi che usi con
nomeOggetto.nomeMetodo(). Sono le funzioni del capitolo 9
con una differenza sola: vedono le proprietà dell'oggetto senza che nessuno gliele
passi come parametri. 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.
10.6 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(): è il modo per dare a una
classe come quella dell'esempio 1 una stampa leggibile, invece di lasciare il testo predefinito.
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, e tieni la class
semplice per gli oggetti che fanno qualcosa più che contenere valori.
10.7 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.
10.8 Riepilogo
| Scrivi | Cosa fa |
|---|---|
class C(val a: Int, var b: String) | dichiara una classe: descrive che dati e che metodi hanno i suoi oggetti |
data class C(val a: Int, var b: String) | come sopra, ma per i dati: toString, ==, copy e destrutturazione arrivano gratis |
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) |
10.9 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).
class con proprietà e metodiclass e data classdata class con proprietà val e vardata classcopy() per creare una variante di un oggettoval (a, b) = oggetto