Capitolo 10

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.

In questo capitolo impari

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:

Come si legge la dichiarazione di una classe

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ì.

Perché non due liste separate?

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.

E se stampi l'oggetto direttamente?

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.

Come si legge la dichiarazione di una data class

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.

  1. println(p) usa il toString() che Kotlin scrive per te: Punto(x=3, y=4), con i nomi delle proprietà.
  2. copy(y = 10) crea un nuovo oggetto cambiando solo y: Punto(x=3, y=10). L'oggetto p non viene toccato.
  3. p == Punto(3, 4) confronta i valori, non l'indirizzo: è true.
  4. La destrutturazione val (a, b) = q mette 3 in a e 10 in b.

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.

  1. Con data class Kotlin scrive per te un == che confronta i valori delle proprietà: i due D(1) sono uguali, quindi true.
  2. Con una class normale quel confronto non c'è: == controlla se i due oggetti sono lo stesso oggetto in memoria, e qui sono due oggetti diversi...
  3. ...quindi C(1) == C(1) è false, anche se i valori sono identici.
data class o class?

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.
Cancelli e ricrei l'oggetto a manoFunziona, ma è più lungo e rischi di dimenticare qualche proprietà: copy fa proprio questo lavoro.

Regola: var nel costruttore = proprietà modificabile; val = per cambiarla serve copy.

copy crea sempre un oggetto nuovo

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.

Funzione con corpo o con =

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.
Due variabili separate, 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).

class e data class non sono intercambiabili in tutto

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

ScriviCosa 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.blegge 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 == ptrue se tutte le proprietà sono uguali (data class)
o.copy(a = 5)nuovo oggetto con a cambiato e il resto uguale
val (x, y) = puntoestrapola 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(...).

  1. La data class dichiara per prima la "forma" del prodotto: le tre proprietà arrivano dal costruttore con val.
  2. fun totale(): Double = prezzo * quantita è un metodo: dentro vede le proprietà come se fossero variabili normali.
  3. I due decimali sul denaro si ottengono con "%.2f".format(...), già visto nel capitolo 8.
  4. 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).

  1. La lista listOf(...) contiene tre oggetti Studente creati con il costruttore.
  2. Nel ciclo for accumuliamo la somma dei voti e cerchiamo il voto più alto, come si fa con i numeri normali: s.voto è un Int.
  3. La media si calcola con somma.toDouble() / classe.size: il toDouble() evita la divisione intera.
  4. migliore.copy(voto = 10) crea un nuovo oggetto con lo stesso nome e voto cambiato: l'oggetto migliore originale resta invariato.
  5. Stampando l'oggetto con "... " + migliore.copy(voto = 10) entra in gioco il toString() 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.

  1. prezzo() usa la forma = if (...) ... else ...: in Kotlin una if può essere un'espressione che restituisce un valore.
  2. Le condizioni si controllano in ordine: prima i minori di 14 anni, poi i maggiori di 64; chi non rientra paga il prezzo pieno.
  3. biglietto() chiama un altro metodo della stessa classe scrivendo semplicemente prezzo(): i metodi si vedono tra loro.
  4. 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.

  1. La data class descrive una riga di magazzino e totale() è un metodo che calcola un valore derivato dalle proprietà: nome, quantità e prezzo viaggiano insieme, invece di essere liste separate da tenere allineate.
  2. La lista si crea vuota con mutableListOf<Riga>() e cresce dentro il ciclo con add(...): i dati arrivano dall'input, quindi non si conoscono in anticipo.
  3. toIntOrNull() ?: 0 gestisce 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.
  4. Somma e ricerca del massimo sono gli algoritmi dei capitoli precedenti applicati a r.totale(); tenere l'oggetto piuCara permette 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.

  1. libro.prezzo = 15.0 è un errore: prezzo è una proprietà val, quindi non si può cambiare dopo la creazione.
  2. copy(prezzo = 15.0) crea un nuovo oggetto con tutti i valori uguali tranne quello indicato.
  3. L'oggetto di partenza resta Libro(titolo=Kotlin, prezzo=20.0): questo è il vantaggio di copy, che non tocca l'originale.
  4. Se il prezzo dovesse cambiare davvero nel tempo, la proprietà andrebbe dichiarata var: data class Libro(val titolo: String, var prezzo: Double).
  • so dichiarare una class con proprietà e metodi
  • so spiegare la differenza fra class e data class
  • so dichiarare una data class con proprietà val e var
  • so creare oggetti e leggerne o modificarne le proprietà
  • so spiegare che cosa Kotlin scrive gratis in una data class
  • so usare copy() per creare una variante di un oggetto
  • so scrivere metodi che leggono le proprietà dell'oggetto
  • so destrutturare un oggetto con val (a, b) = oggetto
  • so leggere un messaggio di errore e correggere il programma da solo