Un Esempio di Programma Che Usa Struct
Per capire quando potremmo voler usare le struct, scriviamo un programma che calcola l’area di un rettangolo. Partiremo usando variabili singole e poi riscriveremo il programma un pezzo per volta finché non useremo le struct.
Creiamo un nuovo progetto binario con Cargo chiamato rettangoli che prenderà la larghezza e l’altezza di un rettangolo specificate in pixel e calcolerà l’area del rettangolo. Il Listato 5-8 mostra un breve programma con un modo per farlo nel file src/main.rs del nostro progetto.
fn main() { let larghezza1 = 30; let altezza1 = 50; println!( "L'area del rettangolo è di {} pixel quadrati.", area(larghezza1, altezza1) ); } fn area(larghezza: u32, altezza: u32) -> u32 { larghezza * altezza }
Ora esegui questo programma usando cargo run
:
$ cargo run
Compiling rettangoli v0.1.0 (file:///progetti/rettangoli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/rettangoli`
L'area del rettangolo è di 1500 pixel quadrati.
Questo codice riesce a calcolare l’area del rettangolo chiamando la funzione
area
con ogni dimensione, ma possiamo fare di più per rendere il codice chiaro
e leggibile.
Il problema con questo codice è evidente nella firma di area
:
fn main() {
let larghezza1 = 30;
let altezza1 = 50;
println!(
"L'area del rettangolo è di {} pixel quadrati.",
area(larghezza1, altezza1)
);
}
fn area(larghezza: u32, altezza: u32) -> u32 {
larghezza * altezza
}
La funzione area
dovrebbe calcolare l’area di un rettangolo singolo, ma la
funzione che abbiamo scritto ha due parametri, e non è chiaro da nessuna parte
nel nostro programma che i parametri siano correlati. Sarebbe più leggibile e
più gestibile raggruppare larghezza e altezza insieme. Abbiamo già discusso un
modo per farlo nella sezione “Il Type Tupla”
del Capitolo 3: usando le tuple.
Riscrivere con le Tuple
Il Listato 5-9 mostra un’altra versione del nostro programma che usa le tuple.
fn main() { let rettangolo1 = (30, 50); println!( "L'area del rettangolo è di {} pixel quadrati.", area(rettangolo1) ); } fn area(dimensioni: (u32, u32)) -> u32 { dimensioni.0 * dimensioni.1 }
Da un lato, questo programma è migliore. Le tuple ci permettono di aggiungere un po’ di struttura, e ora stiamo passando un solo argomento. Ma dall’altro, questa versione è meno chiara: le tuple non nominano i loro elementi, quindi dobbiamo indicizzare le parti della tupla, rendendo il nostro calcolo meno ovvio.
Confondere larghezza e altezza non avrebbe importanza per il calcolo dell’area,
ma se volessimo disegnare il rettangolo sullo schermo, importerebbe! Dovremmo
tenere a mente che larghezza
è l’indice della tupla 0
e altezza
è l’indice
della tupla 1
. Questo sarebbe ancora più difficile da capire e ricordare per
qualcun altro che in futuro leggesse o usasse il nostro codice. Poiché non
abbiamo reso palese il significato dei nostri dati nel codice, è più facile
introdurre errori.
Riscrivere con le Struct
Usiamo la struct per aggiungere significato etichettando i dati. Possiamo trasformare la tupla che stiamo usando in una struct con un nome per l’intero e nomi per le parti, come mostrato nel Listato 5-10.
struct Rettangolo { larghezza: u32, altezza: u32, } fn main() { let rettangolo1 = Rettangolo { larghezza: 30, altezza: 50, }; println!( "L'area del rettangolo è di {} pixel quadrati.", area(&rettangolo1) ); } fn area(rettangolo: &Rettangolo) -> u32 { rettangolo.larghezza * rettangolo.altezza }
Rettangolo
Qui abbiamo definito una struct e l’abbiamo chiamata Rettangolo
. All’interno
delle parentesi graffe, abbiamo definito i campi come larghezza
e altezza
,
entrambi di type u32
. Poi, in main
, abbiamo creato un’istanza particolare
di Rettangolo
che ha larghezza 30
e altezza 50
.
La nostra funzione area
è ora definita con un solo parametro, che abbiamo
chiamato Rettangolo
, il cui type è un reference immutabile a un’istanza
della struct Rettangolo
. Come menzionato nel Capitolo 4, ci serve solo
prendere in prestito la struct piuttosto che averne la ownership. In questo
modo, main
mantiene la sua ownership e può continuare a usare rettangolo1
,
che è il motivo per cui usiamo &
nella firma della funzione e dove chiamiamo
la funzione.
La funzione area
accede ai campi larghezza
e altezza
dell’istanza di
Rettangolo
(nota che accedere ai campi di un’istanza di struct presa in
prestito non muove i valori dei campi, motivo per cui spesso si vedono
reference di struct). La nostra firma della funzione per area
ora dice
esattamente ciò che intendiamo: calcolare l’area di Rettangolo
, usando i suoi
campi larghezza
e altezza
. Questo comunica che larghezza e altezza sono
correlate tra loro e fornisce nomi descrittivi ai valori invece di usare gli
indici della tupla 0
e 1
. Questo è un vantaggio in termini di chiarezza.
Aggiungere Funzionalità con i Trait Derivati
Sarebbe utile poter stampare un’istanza di Rettangolo
mentre eseguiamo il
debug del nostro programma e vedere i valori di tutti i suoi campi. Il Listato
5-11 prova a usare la macro println!
come l’abbiamo
usata nei capitoli precedenti. Questo però non funzionerà.
struct Rettangolo {
larghezza: u32,
altezza: u32,
}
fn main() {
let rettangolo1 = Rettangolo {
larghezza: 30,
altezza: 50,
};
println!("rettangolo1 è {rettangolo1}");
}
Rettangolo
Quando compiliamo questo codice, otteniamo un errore con questo messaggio principale:
error[E0277]: `Rettangolo` doesn't implement `std::fmt::Display`
La macro println!
può fare molti tipi di formattazione e, come impostazione
predefinita, le parentesi graffe dicono a println!
di usare una formattazione
conosciuta come Display
, output pensato per il l’utente finale che utilizzerà
il programma. I type primitivi che abbiamo visto finora implementano Display
di default perché c’è un solo modo in cui vorresti mostrare un 1
o qualsiasi
altro type primitivo a un utente. Ma con le struct il modo in cui println!
dovrebbe formattare l’output è meno chiaro perché ci sono più possibilità di
visualizzazione: vuoi le virgole o no? Vuoi stampare le parentesi graffe? Devono
essere mostrati tutti i campi? A causa di questa ambiguità, Rust non cerca di
indovinare ciò che vogliamo, e le struct non hanno un’implementazione standard
di Display
da usare con println!
e il segnaposto {}
.
Se continuiamo a leggere gli errori, troveremo questa nota utile:
= help: the trait `std::fmt::Display` is not implemented for `Rettangolo`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Proviamolo! La chiamata alla macro println!
ora assomiglierà a
println!("rettangolo1 è {rettangolo1:?}");
. Inserire lo specificatore :?
all’interno delle parentesi graffe dice a println!
che vogliamo usare un
formato di output chiamato Debug
. Il trait Debug
ci permette di stampare
la nostra struct in un modo utile per gli sviluppatori, così possiamo vedere
il suo valore mentre eseguiamo il debug del nostro codice.
Compila il codice con questa modifica. Accidenti! Otteniamo ancora un errore:
error[E0277]: `Rettangolo` doesn't implement `Debug`
Ma di nuovo, il compilatore ci dà una nota utile:
= help: the trait `Debug` is not implemented for `Rettangolo`
= note: add `#[derive(Debug)]` to `Rettangolo` or manually `impl Debug for Rettangolo`
Rust include effettivamente funzionalità per stampare informazioni di debug,
ma dobbiamo esplicitamente dichiararlo per rendere disponibile quella
funzionalità alla nostra struct. Per farlo, aggiungiamo l’attributo esterno
#[derive(Debug)]
appena prima della definizione della struct, come mostrato
nel Listato 5-12.
#[derive(Debug)] struct Rettangolo { larghezza: u32, altezza: u32, } fn main() { let rettangolo1 = Rettangolo { larghezza: 30, altezza: 50, }; println!("rettangolo1 è {rettangolo1:?}"); }
Debug
e stampare Rettangolo
usando la formattazione di debugOra quando eseguiamo il programma, non otterremo errori e vedremo il seguente output:
$ cargo run
Compiling rettangoli v0.1.0 (file:///progetti/rettangoli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/rettangoli`
rettangolo1 è Rettangolo { larghezza: 30, altezza: 50 }
Bene! Non è l’output più bello, ma mostra i valori di tutti i campi per questa
istanza, il che aiuterebbe sicuramente durante lo sviluppo e il debug del
programma. Quando abbiamo struct più grandi, è utile avere un output un po’
più facile da leggere; in quei casi, possiamo usare {:#?}
invece di {:?}
nella stringa di println!
. In questo esempio, usare lo stile {:#?}
produrrà
il seguente output:
$ cargo run
Compiling rettangoli v0.1.0 (file:///progetti/rettangoli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/rettangoli`
rettangolo1 è Rettangolo {
larghezza: 30,
altezza: 50,
}
Un altro modo per stampare un valore usando il formato Debug
è usare la macro
dbg!
, che prende ownership di un’espressione (a differenza di
println!
, che prende un reference), stampa file e numero di linea di dove
quella chiamata a dbg!
si verifica nel codice insieme al valore risultante di
quell’espressione, e restituisce l’ownership del valore.
Nota: Chiamare la macro dbg! stampa sullo stream di errore standard (
stderr
), a differenza diprintln!
, che stampa sullo stream di output standard (stdout
). Parleremo meglio distderr
estdout
nella sezione “Scrivere i Messaggi di Errore su Standard Error invece che su Standard Output” del Capitolo 12.
Ecco un esempio in cui siamo interessati al valore che viene assegnato al campo
larghezza
, così come al valore dell’intera struct in rettangolo1
:
#[derive(Debug)] struct Rettangolo { larghezza: u32, altezza: u32, } fn main() { let scala = 2; let rettangolo1 = Rettangolo { larghezza: dbg!(30 * scala), altezza: 50, }; dbg!(&rettangolo1); }
Possiamo mettere dbg!
attorno all’espressione 30 * scala
e, poiché dbg!
restituisce l’ownership del valore dell’espressione, il campo larghezza
otterrà lo stesso valore come se non avessimo la chiamata a dbg!
lì. Non
vogliamo che dbg!
prenda ownership di rettangolo1
, quindi usiamo un
riferimento a rettangolo1
nella chiamata successiva. Ecco come appare l’output
di questo esempio:
$ cargo run
Compiling rettangoli v0.1.0 (file:///progetti/rettangoli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/rettangoli`
[src/main.rs:10:20] 30 * scala = 60
[src/main.rs:14:5] &rettangolo1 = Rettangolo {
larghezza: 60,
altezza: 50,
}
Possiamo vedere che il primo frammento di output proviene da src/main.rs riga
10 dove stiamo facendo il debug dell’espressione 30 * scala
, e il suo valore
risultante è 60
(la formattazione Debug
implementata per gli integer è di
stampare solo il loro valore). La chiamata a dbg!
alla riga 14 di
src/main.rs stampa il valore di &rettangolo1
, che è la struct
Rettangolo
. Questo output usa la formattazione Debug
“pretty” del type
Rettangolo
. La macro dbg!
può essere davvero utile quando stai cercando di
capire cosa sta facendo il tuo codice!
Oltre al trait Debug
, Rust fornisce diversi trait che possiamo usare con
l’attributo derive
che possono aggiungere comportamenti utili ai nostri type
personalizzati. Quei trait e i loro comportamenti sono elencati
nell’Appendice C. Tratteremo come implementare questi
trait con un comportamento personalizzato e come creare i propri trait nel
Capitolo 10. Ci sono anche molti attributi oltre a derive
; per maggiori
informazioni, vedi la sezione “Attributes” del Rust Reference.
La nostra funzione area
è molto specifica: calcola solo l’area dei rettangoli.
Sarebbe utile legare questo comportamento più strettamente alla nostra struct
Rettangolo
perché non funzionerà con altri type. Vediamo come possiamo
continuare a riscrivere questo codice trasformando la funzione area
in un
metodo (method) definito sul nostro type Rettangolo
.