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.
- una classe descrive un tipo di cosa; gli oggetti sono le copie;
- la
data classè la forma comoda quando vuoi soprattutto tenere dei dati; - un metodo è una funzione dentro la classe: vede già le proprietà.
copy, stampa e confronto li vedi dopo, un pezzo alla volta.
Ripasso lampo: stringhe ed elenchi
Prima di unire i dati in un solo oggetto, ripassa due cose che già sai.
Dal capitolo 8: prendere un pezzo di testo.
Dal capitolo 9: una funzione con parametri e return.
Un metodo, tra poco, è proprio una funzione scritta dentro la classe.
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 doppio(n: Int): Int = n * 2
fun main() {
val x = doppio(5)
println(x + 1)
}11Esatto: doppio(5) consegna 10, poi si aggiunge 1.10Quello è solo il risultato di doppio(5). Poi il programma fa x + 1.5No: doppio restituisce il doppio, non il numero di partenza.La funzione consegna un valore a chi la chiama. Quel valore si può mettere in una variabile e usare subito dopo. Un metodo, in questo capitolo, è la stessa idea: una funzione scritta dentro la classe.
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.
Fino ad ora hai stampato le singole proprietà (libro.titolo, libro.prezzo),
ma puoi stampare anche l'oggetto intero con println(libro). Con la
class normale dell'Esempio 1 non avresti niente di leggibile: comparirebbe qualcosa
come Studente@1, il nome della classe con un codice dell'oggetto. Con la
data class, invece, la stampa mostra tutti i valori in un colpo solo:
Libro(titolo=Il piccolo principe, autore=Saint-Exupery, prezzo=12.5). È il regalo
della data class: il toString() che costruisce questo testo lo scrive
Kotlin per te.
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.
Nei prossimi riquadri vedi prima come si stampa un oggetto.
copy e lo “spacchettamento” (destrutturazione) arrivano nelle sezioni 10.4 e 10.7:
in questa previsione puoi già osservarli, ma non serve saperli scrivere ancora.
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.
Per tenere insieme dei dati, di solito basta data class. Anche se un campo è var.
Usa class senza data quando vuoi decidere tu la stampa, come il
Contatore più sotto.
L'esempio 1 (class Studente) mostra la forma lunga. Poteva essere anche una data class.
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 dati fra parentesi, puoi aggiungere altre variabili dentro le graffe della classe.
Il blocco init è un pezzo che gira appena crei l'oggetto:
qui serve solo a stampare un messaggio.
In una class normale la stampa di default è illeggibile.
Allora scrivi tu toString. La parola override dice a Kotlin:
«uso la mia versione, non la tua».
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: con data class stampa e confronto == funzionano già sui dati, senza scriverli tu.class normaleCon una class normale devi scrivere tu come si stampa e come si confronta. Per un contenitore di dati è più lavoro del necessario.nome e votoFunziona per un solo studente. Appena ne hai tre, perdi il nesso fra nome e voto.Usa data class quando l'oggetto tiene dei dati; una class normale quando fa qualcosa 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.
Crea l'oggetto. Stampa quattro righe: nome, prezzo, quantità e totale, con due decimali sul denaro.
Con Quaderno, 2.5 e 3:
Nome: Quaderno, Prezzo: 2.50, Quantità: 3,
Totale: 7.50.
Non serve l'IVA. Non serve un secondo metodo.
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. fun totale(): Double = prezzo * quantitaè un metodo: dentro vede le proprietà come se fossero variabili normali.- I due decimali sul denaro si ottengono con
"%.2f".format(...), già visto nel capitolo 8. - Dopo aver descritto il tipo, il programma legge i tre dati e costruisce l'oggetto con
Prodotto(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).
Le stampe sono: titolo === PAGELLA ===, tre righe Anna: 8 ecc.,
Media: 7.33, Migliore: Sara con 9 e
Con lode: Studente(nome=Sara, 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 (titolo === BIGLIETTO === e una riga
Marta (12 anni) -> 5.00 euro) e poi, con copy, il prezzo
che pagherebbe la stessa persona se avesse 70 anni:
Con 70 anni: Marta (70 anni) -> 6.00 euro.
Puoi mettere il testo del biglietto in un metodo biglietto().
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)crea un nuovo spettatore cambiando solo l'età: il prezzo si ricalcola da solo.
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: non tre numeri sulla stessa riga). Metti gli articoli in una lista.
Stampa il titolo === MAGAZZINO ===, poi l'elenco, una riga per articolo, nel formato
Quaderno: 3 x 2.50 = 7.50 euro.
Poi stampa esattamente:
Totale: 47.40 euro e
Riga più cara: Zaino (39.90 euro).
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 di prova (9 righe: Quaderno / 3 / 2.5, Zaino / 1 / 39.9, Penne / riga vuota / 0.8) il totale delle tre righe è 7.50 + 39.90 + 0.00 = 47.40. La riga più cara è lo Zaino.
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