Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

File: src/main.rs
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
}
Listato 5-8: Calcolo dell’area di un rettangolo specificando in variabili separate larghezza e 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.

File: src/main.rs
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
}
Listato 5-9: Specificare larghezza e altezza di un rettangolo tramite una tupla

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.

File: src/main.rs
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
}
Listato 5-10: Definizione di una struct 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à.

File: src/main.rs
struct Rettangolo {
    larghezza: u32,
    altezza: u32,
}

fn main() {
    let rettangolo1 = Rettangolo {
        larghezza: 30,
        altezza: 50,
    };

    println!("rettangolo1 è {rettangolo1}");
}
Listato 5-11: Tentativo di stampare un’istanza di 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.

File: src/main.rs
#[derive(Debug)]
struct Rettangolo {
    larghezza: u32,
    altezza: u32,
}

fn main() {
    let rettangolo1 = Rettangolo {
        larghezza: 30,
        altezza: 50,
    };

    println!("rettangolo1 è {rettangolo1:?}");
}
Listato 5-12: Aggiunta dell’attributo per derivare il trait Debug e stampare Rettangolo usando la formattazione di debug

Ora 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 di println!, che stampa sullo stream di output standard (stdout). Parleremo meglio di stderr e stdout 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.