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

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:

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.

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.

  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?

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.
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 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().
Un 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).

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. 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(...).

  1. La data class dichiara per prima la "forma" del prodotto: le tre proprietà arrivano dal costruttore con val. Prima il tipo, poi l'uso.
  2. fun totale(): Double = prezzo * quantita è un metodo: dentro vede le proprietà come se fossero variabili normali.
  3. scheda() mette insieme testo e numeri usando il template $nome e la formattazione "%.2f".format(...).
  4. Dopo aver descritto il tipo, il programma legge i tre dati (readln() per il nome, readlnOrNull()?.toDoubleOrNull() ?: 0.0 per il prezzo) 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).

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

  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) e copy(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.

  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