Un Programma Esempio Usando Struct

Per capire quando potremmo voler usare Struct, scriviamo un programma che calcola l'area di un rettangolo. Inizieremo usando variabili singole e poi rifattoreremo il programma fino a usare Struct.

Creiamo un nuovo progetto binario con Cargo chiamato rectangles che prenderà la larghezza e l'altezza di un rettangolo specificato in pixel e calcolerà l'area del rettangolo. Il Listing 5-8 mostra un breve programma con un modo per fare esattamente ciò nel file src/main.rs del nostro progetto.

Nome del file: src/main.rs

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Listing 5-8: Calcolare l'area di un rettangolo specificato da variabili larghezza e altezza separate

Ora esegui questo programma usando cargo run:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

Questo codice riesce a calcolare l'area del rettangolo chiamando la funzione area con ciascuna dimensione, ma possiamo fare di più per rendere questo codice chiaro e leggibile.

Il problema con questo codice è evidente nella firma della funzione area:

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

La funzione area dovrebbe calcolare l'area di un solo rettangolo, 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 Tipo Tuple" del Capitolo 3: usando le tuple.

Rifattorizzare con Tuple

Il Listing 5-9 mostra un'altra versione del nostro programma che utilizza le tuple.

Nome del file: src/main.rs

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

Listing 5-9: Specificare la larghezza e l'altezza del rettangolo con una tupla

In un certo senso, questo programma è migliore. Le tuple ci permettono di aggiungere un po' di struttura, e ora stiamo passando solo un argomento. Ma in un altro modo, 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 la larghezza e l'altezza non importerebbe per il calcolo dell'area, ma se vogliamo disegnare il rettangolo sullo schermo, importerebbe! Dovremmo tener a mente che width è l'indice di tupla 0 e height è l'indice di tupla 1. Questo sarebbe ancora più difficile per qualcun altro da capire e tenere a mente se usasse il nostro codice. Poiché non abbiamo trasmesso il significato dei nostri dati nel codice, è ora più facile introdurre errori.

Rifattorizzare con Struct: Aggiungere Più Significato

Usiamo Struct per aggiungere significato etichettando i dati. Possiamo trasformare la tupla che stiamo usando in una Struct con un nome per l'intero così come nomi per le parti, come mostrato nel Listing 5-10.

Nome del file: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

Listing 5-10: Definire una Struct Rectangle

Qui abbiamo definito una Struct e l'abbiamo chiamata Rectangle. Dentro le parentesi graffe, abbiamo definito i campi come width e height, entrambi di tipo u32. Poi, in main, abbiamo creato una particolare istanza di Rectangle che ha una larghezza di 30 e un'altezza di 50.

La nostra funzione area è ora definita con un parametro, che abbiamo chiamato rectangle, il cui tipo è un prestito immutabile di una istanza di Rectangle. Come menzionato nel Capitolo 4, vogliamo prendere in prestito la Struct piuttosto che prendere proprietà di essa. In questo modo, main mantiene la sua proprietà e può continuare a usare rect1, il che è il motivo per cui usiamo & nella firma della funzione e dove chiamiamo la funzione.

La funzione area accede ai campi width e height della istanza Rectangle (nota che accedere ai campi di una istanza di Struct presa in prestito non sposta i valori dei campi, motivo per cui spesso si vedono prestiti di Struct). La nostra firma della funzione per area ora dice esattamente cosa intendiamo: calcolare l'area di Rectangle, usando i suoi campi width e height. Questo comunica che la larghezza e l'altezza sono correlate tra loro, e dà nomi descrittivi ai valori piuttosto che usare i valori di indice della tupla 0 e 1. È una vittoria per la chiarezza.

Aggiungere Funzionalità Utili con i Trait Derivati

Sarebbe utile poter stampare un'istanza di Rectangle mentre stiamo debuggando il nostro programma e vedere i valori di tutti i suoi campi. Il Listing 5-11 cerca di usare la macro println! come abbiamo fatto nei capitoli precedenti. Questo però non funzionerà.

Nome del file: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);
}

Listing 5-11: Tentativo di stampare un'istanza di Rectangle

Quando compiliamo questo codice, otteniamo un errore con questo messaggio centrale:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

La macro println! può fare molti tipi di formattazione, e per impostazione predefinita, le parentesi graffe indicano a println! di usare formattazione nota come Display: output previsto per il consumo diretto dell'utente finale. I tipi primitivi che abbiamo visto finora implementano Display per impostazione predefinita perché c'è solo un modo in cui si vorrebbe mostrare un 1 o qualsiasi altro tipo 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 virgole o no? Vuoi stampare le parentesi graffe? Dovrebbero essere mostrati tutti i campi? A causa di questa ambiguità, Rust non cerca di indovinare cosa vogliamo, e le Struct non hanno un'implementazione fornita 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 `Rectangle`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

Proviamolo! La chiamata alla macro println! sembrerà ora println!("rect1 è {rect1:?}");. Mettere lo specificatore :? dentro le parentesi graffe indica a println! che vogliamo usare un formato di output chiamato Debug. Il Trait Debug ci consente di stampare la nostra Struct in un modo utile per gli sviluppatori, così possiamo vedere il suo valore mentre stiamo debugando il nostro codice.

Compila il codice con questo cambiamento. Maledizione! Riceviamo ancora un errore:

error[E0277]: `Rectangle` doesn't implement `Debug`

Ma ancora, il compilatore ci dà una nota utile:

   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`

Rust inclus la funzionalità per stampare informazioni di debugging, ma dobbiamo optare esplicitamente per rendere disponibile quella funzionalità per la nostra Struct. Per farlo, aggiungiamo l'attributo esterno #[derive(Debug)] appena prima della definizione della Struct, come mostrato nel Listing 5-12.

Nome del file: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1);
}

Listing 5-12: Aggiunta dell'attributo per derivare il Trait Debug e stampa dell'istanza di Rectangle usando la formattazione per il debug

Ora quando eseguiamo il programma, non otterremo errori, e vedremo il seguente output:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Bello! Non è l'output più bello, ma mostra i valori di tutti i campi di questa istanza, il che sarebbe sicuramente utile durante il debugging. Quando abbiamo Struct più grandi, è utile avere un output un po' più facile da leggere; in questi casi, possiamo usare {:#?} invece di {:?} nella stringa di println!. In questo esempio, usando lo stile {:#?} si otterrà il seguente output:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

Un altro modo per stampare un valore usando il formato Debug è usare la macro dbg!, che prende proprietà di un'espressione (a differenza di println!, che prende un riferimento), stampa il file e il numero di riga dove quella chiamata alla macro dbg! si verifica nel codice insieme al valore risultante di quella espressione, e restituisce la proprietà del valore.

Nota: Chiamare la macro dbg! stampa allo stream del console error standard (stderr), al contrario di println!, che stampa allo stream del console output standard (stdout). Parleremo di più di stderr e stdout nella sezione "Scrittura di Messaggi di Errore su Error Standard invece di Output Standard" nel Capitolo 12.

Ecco un esempio dove siamo interessati al valore che viene assegnato al campo width, così come il valore della intera Struct in rect1:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

Possiamo mettere dbg! intorno all'espressione 30 * scale e, poiché dbg! restituisce la proprietà del valore dell'espressione, il campo width otterrà lo stesso valore come se non avessimo la chiamata dbg! lì. Non vogliamo che dbg! prenda la proprietà di rect1, quindi usiamo un riferimento a rect1 nella chiamata successiva. Ecco come appare l'output di questo esempio:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Possiamo vedere che il primo pezzo di output viene da src/main.rs linea 10 dove stiamo debuggando l'espressione 30 * scale, e il suo valore risultante è 60 (la formattazione Debug implementata per gli interi è stampare solo il loro valore). La chiamata a dbg! sulla linea 14 di src/main.rs stampa il valore di &rect1, che è la Struct Rectangle. Questo output usa la formattazione Debug "pretty" del tipo Rectangle. La macro dbg! può essere molto utile quando stai cercando di capire cosa sta facendo il tuo codice!

Oltre al Trait Debug, Rust ha fornito diversi Traits da usare con l'attributo derive che possono aggiungere comportamento utile ai nostri tipi personalizzati. Quei Traits e i loro comportamenti sono elencati in Appendice C. Tratteremo come implementare questi Traits con comportamento personalizzato così come come creare i tuoi Traits nel Capitolo 10. Ci sono anche molti attributi diversi da derive; per ulteriori informazioni, consulta la sezione "Attributi" del Riferimento Rust.

La nostra funzione area è molto specifica: calcola solo l'area dei rettangoli. Sarebbe utile legare questo comportamento più strettamente alla nostra Struct Rectangle perché non funzionerà con nessun altro tipo. Vediamo come possiamo continuare a refattorizzare questo codice trasformando la funzione area in un metodo area definito sul nostro tipo Rectangle.