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

Definire e Istanziare le Struct

Le struct sono simili alle tuple, discussi nella sezione “Il Type Tupla”, in quanto entrambi possono contenere più valori correlati. Come per le tuple, i componenti di una struct possono essere di type diversi. A differenza delle tuple, in una struct puoi denominare ogni pezzo di dati in modo che sia chiaro il significato dei valori. L’aggiunta di questi nomi significa che le struct sono più flessibili delle tuple: non devi fare affidamento sull’ordine dei dati per specificare o accedere ai valori di un’istanza.

Per definire una struct, inseriamo la parola chiave struct e diamo un nome all’intera struct. Il nome di una struct dovrebbe descrivere il significato dei dati raggruppati insieme. Poi, all’interno di parentesi graffe, definiamo i nomi e i type dei pezzi di dati, che chiamiamo campi (field in inglese). Ad esempio, il Listato 5-1 mostra una struct che memorizza informazioni su un account utente.

File: src/main.rs
struct Utente {
    attivo: bool,
    nome_utente: String,
    email: String,
    numero_accessi: u64,
}

fn main() {}
Listato 5-1: Una definizione della struct Utente

Per utilizzare una struct dopo averla definita, creiamo un’istanza di quella struct specificando valori concreti per ciascuno dei campi. Creiamo un’istanza indicando il nome della struct e poi aggiungendo parentesi graffe contenenti coppie chiave: valore, dove le chiavi sono i nomi dei campi e i valori sono i dati che vogliamo memorizzare in quei campi. Non dobbiamo specificare i campi nello stesso ordine in cui li abbiamo dichiarati nella struct. In altre parole, la definizione della struct è come un modello generale per il type, e le istanze riempiono quel modello con dati particolari per creare valori del type. Ad esempio, possiamo dichiarare un utente particolare come mostrato nel Listato 5-2.

File: src/main.rs
struct Utente {
    attivo: bool,
    nome_utente: String,
    email: String,
    numero_accessi: u64,
}

fn main() {
    let utente1 = Utente {
        attivo: true,
        nome_utente: String::from("qualcuno123"),
        email: String::from("qualcuno@mia_mail.com"),
        numero_accessi: 1,
    };
}
Listato 5-2: Creazione di un’istanza della struct Utente

Per ottenere un valore specifico da una struct, usiamo la notazione col punto. Ad esempio, per accedere all’indirizzo email di questo utente, usiamo utente1.email. Se l’istanza è mutabile, possiamo cambiare un valore usando la notazione col punto assegnando un valore a un campo in particolare. Il Listato 5-3 mostra come modificare il valore del campo email di un’istanza Utente mutabile.

File: src/main.rs
struct Utente {
    attivo: bool,
    nome_utente: String,
    email: String,
    numero_accessi: u64,
}

fn main() {
    let mut utente1 = Utente {
        attivo: true,
        nome_utente: String::from("qualcuno123"),
        email: String::from("qualcuno@mia_mail.com"),
        numero_accessi: 1,
    };

    user1.email = String::from("nuova_email@mia_mail.com");
}
Listato 5-3: Cambiare valore del campo email di un’istanza Utente

Nota che l’intera istanza deve essere mutabile; Rust non ci permette di contrassegnare solo alcuni campi come mutabili. Come per qualsiasi espressione, possiamo costruire una nuova istanza della struct come ultima espressione nel corpo di una funzione per restituire implicitamente quella nuova istanza.

Il Listato 5-4 mostra la funzione nuovo_utente che restituisce un’istanza Utente con l’email e il nome utente indicati. Il campo attivo assume il valore true e numero_accessi prende il valore di 1.

File: src/main.rs
struct Utente {
    attivo: bool,
    nome_utente: String,
    email: String,
    numero_accessi: u64,
}

fn nuovo_utente(email: String, nuome_utente: String) -> Utente {
    User {
        attivo: true,
        nuome_utente: nuome_utente,
        email: email,
        numero_accessi: 1,
    }
}

fn main() {
    let utente1 = nuovo_utente(
        String::from("qualcuno@mia_mail.com"),
        String::from("qualcuno123"),
    );
}
Listato 5-4: Una funzione nuovo_utente che prende una email e un nome utente per ritornare un’istanza Utente

Ha senso chiamare i parametri della funzione con lo stesso nome dei campi della struct, ma dover ripetere i nomi dei campi email e nome_utente e delle variabili è un po’ noioso. Se la struct avesse più campi, la ripetizione di ogni nome diventerebbe ancora più fastidiosa. Per fortuna esiste una comoda scorciatoia!

Utilizzare la Sintassi Abbreviata di Inizializzazione

Poiché i nomi dei parametri e i nomi dei campi della struct sono esattamente gli stessi, possiamo usare la sintassi di inizializzazione abbreviata dei campi (field init shorthand) per riscrivere la funzione nuovo_utente in modo che si comporti esattamente allo stesso modo ma senza la ripetizione di nome_utente e email, come mostrato nel Listato 5-5.

File: src/main.rs
struct Utente {
    attivo: bool,
    nome_utente: String,
    email: String,
    numero_accessi: u64,
}

fn nuovo_utente(email: String, nome_utente: String) -> Utente {
    Utente {
        attivo: true,
        nome_utente,
        email,
        numero_accessi: 1,
    }
}

fn main() {
    let utente1 = nuovo_utente(
        String::from("qualcuno@mia_mail.com"),
        String::from("qualcuno123"),
    );
}
Listato 5-5: Una funzione nuovo_utente che usa la sintassi abbreviata perché i campi e i parametri nome_utente e email hanno lo stesso nome

Qui stiamo creando una nuova istanza della struct Utente, che ha un campo chiamato email. Vogliamo impostare il valore del campo email sul valore del parametro email della funzione nuovo_utente. Dato che il campo email e il parametro email hanno lo stesso nome, dobbiamo solo scrivere email invece di email: email.

Creare Istanze con la Sintassi di Aggiornamento delle Struct

Spesso è utile creare una nuova istanza di una struct che include la maggior parte dei valori da un’altra istanza dello stesso type, ma con alcune modifiche. Puoi farlo usando la sintassi di aggiornamento delle struct (struct update).

Per prima cosa, nel Listato 5-6 mostriamo come creare regolarmente una nuova istanza Utente in utente2, senza la sintassi di aggiornamento. Impostiamo un nuovo valore per email ma per il resto utilizziamo gli stessi valori di utente1 creati nel Listato 5-2.

File: src/main.rs
struct Utente {
    attivo: bool,
    nome_utente: String,
    email: String,
    numero_accessi: u64,
}

fn main() {
    // --taglio--

    let utente1 = Utente {
        email: String::from("qualcuno@mia_mail.com"),
        nome_utente: String::from("qualcuno123"),
        attivo: true,
        numero_accessi: 1,
    };

    let utente2 = Utente {
        attivo: utente1.attivo,
        nome_utente: utente1.nome_utente,
        email: String::from("altra_mail@example.com"),
        numero_accessi: utente1.numero_accessi,
    };
}
Listato 5-6: Creazione di una nuova istanza Utente con gli stessi valori tranne uno di utente1

Utilizzando la sintassi di aggiornamento, possiamo ottenere lo stesso effetto con meno codice, come mostrato nel Listato 5-7. La sintassi .. specifica che i restanti campi non impostati esplicitamente dovrebbero avere lo stesso valore dei campi nell’istanza data.

File: src/main.rs
struct Utente {
    attivo: bool,
    nome_utente: String,
    email: String,
    numero_accessi: u64,
}

fn main() {
    // --taglio--

    let utente1 = Utente {
        email: String::from("qualcuno@mia_mail.com"),
        nome_utente: String::from("qualcuno123"),
        attivo: true,
        numero_accessi: 1,
    };

    let utente2 = Utente {
        email: String::from("altra_mail@example.com"),
        ..utente1
    };
}
Listato 5-7: Utilizzo della sintassi struct update per impostare un nuovo valore di email per un’istanza di Utente, ma utilizzando o restanti valori da utente1

Anche il codice nel Listato 5-7 crea un’istanza in utente2 che ha un valore diverso per email ma ha gli stessi valori per i campi nome_utente, attivo e numero_accessi di utente1. La parola chiave ..utente1 deve venire per ultima per specificare che tutti i campi rimanenti dovrebbero ottenere i propri valori dai campi corrispondenti in utente1, ma possiamo scegliere di specificare i valori per tutti i campi che vogliamo in qualsiasi ordine, indipendentemente dall’ordine dei campi nella definizione della struct.

Nota che la sintassi di aggiornamento utilizza = come un assegnazione; questo perché sposta i dati, proprio come abbiamo visto nella sezione ”Interazione tra Variabili e Dati con Move. In questo esempio, non possiamo più utilizzare utente1 dopo aver creato utente2 perché la String nel campo nome_utente di utente1 è stata spostata in utente2. Se avessimo fornito a utente2 nuovi valori String sia per l’email che per nome_utente e quindi avessimo utilizzato solo i valori attivo e numero_accessi di utente1, utente1 sarebbe ancora valido dopo aver creato utente2. Sia attivo che numero_accessi sono type che implementano il trait Copy, quindi si applicherebbe il comportamento discusso nella sezione ”Duplicare dati sullo Stack. In questo esempio possiamo ancora utilizzare utente1.email, perché il suo valore non è stato spostato da utente1.

Creare Type Diversi con Struct Tupla

Rust supporta anche struct che assomigliano alle tuple, chiamate struct tupla (tuple struct). Le struct tupla hanno il significato aggiuntivo che il nome della struct fornisce, ma non hanno nomi associati ai loro campi; piuttosto, hanno solo i type dei campi. Le struct tupla sono utili quando si vuole dare un nome all’intera tupla e renderla un type diverso da altre tuple, e quando denominare ogni campo come in una struct regolare sarebbe poco utile o ridondante. Per definire una struct tupla, inizia con la parola chiave struct e il nome della struct seguito dai type della tupla. Ad esempio, qui definiamo e utilizziamo due struct tupla chiamate Colore e Punto:

File: src/main.rs
struct Colore(i32, i32, i32);
struct Punto(i32, i32, i32);

fn main() {
    let nero = Colore(0, 0, 0);
    let origine = Punto(0, 0, 0);
}

Tieni presente che i valori nero e origine sono di type diverso perché sono istanze di struct tupla diverse. Ogni struct che definisci diventa un nuovo type a sé stante, anche se i campi all’interno della struct potrebbero avere gli stessi type. Ad esempio, una funzione che accetta un parametro di type Colore non può accettare un Punto come argomento, anche se entrambi i type sono costituiti da tre valori i32. Oltretutto, le istanze di una struct tupla sono simili alle tuple in quanto puoi destrutturarle nelle loro singole parti e puoi utilizzare un . seguito dall’indice per accedere a un singolo valore. A differenza delle tuple però, le struct tupla richiedono di nominare il type di struct quando le destrutturi. Ad esempio, scriveremo let Punto(x, y, z) = origine; per destrutturare i valori del Punto origine in variabili chiamate x, y e z.

Definire Struct Unit

Puoi anche definire struct che non hanno campi! Queste sono chiamate struct unit (unit-like struct) perché si comportano in modo simile a (), il type unit menzionato nella sezione “Il Type Tupla”. Le struct unit possono essere utili quando è necessario implementare un trait su un type ma non si hanno dati che si vogliono memorizzare nel type stesso.

Parleremo dei trait nel Capitolo 10. Ecco un esempio di dichiarazione e istanziazione di una struct unit chiamata SempreUguale:

File: src/main.rs
struct SempreUguale;

fn main() {
    let suggetto = SempreUguale;
}

Per definire SempreUguale, utilizziamo la parola chiave struct, il nome che vogliamo e quindi un punto e virgola. Non c’è bisogno di parentesi graffe o tonde! Quindi possiamo ottenere un’istanza di SempreUguale nella variabile soggetto in un modo simile: utilizzando il nome che abbiamo definito, senza parentesi graffe o tonde. Immagina che in seguito implementeremo il comportamento per questo type in modo tale che ogni istanza di SempreUguale sia sempre uguale a ogni istanza di qualsiasi altro type, magari per avere un risultato noto a scopo di test. Non avremmo bisogno di dati per implementare quel comportamento! Vedremo nel Capitolo 10 come definire i trait e implementarli su qualsiasi type, comprese le struct unit.

Ownership dei Dati di Struct

Nella definizione della struct Utente, abbiamo utilizzato il type String invece del type slice di stringa &str. Questa è una scelta deliberata perché vogliamo che ogni istanza di questa struct possieda tutti i suoi dati e che tali dati siano validi per tutto il tempo in cui la struct è valida.

È anche possibile che le struct memorizzino reference a dati posseduti da qualcos’altro, ma per farlo è necessario l’uso di lifetime, una funzionalità di Rust di cui parleremo nel Capitolo 10. Lifetime garantisce che i dati a cui fa riferimento una struct siano validi finché lo è la struct. Supponiamo che provi a memorizzare un reference in una struct senza specificare la lifetime, come nel seguente esempio in src/main.rs; questo non funzionerà:

File: src/main.rs
struct Utente {
    attivo: bool,
    nome_utente: &str,
    email: &str,
    numero_accessi: u64,
}

fn main() {
    let user1 = Utente {
        attivo: true,
        nome_utente: "qualcuno123",
        email: "qualcuno@mia_mail.com",
        numero_accessi: 1,
    };
}

Il compilatore si lamenterà richiedendo degli identificatori di lifetime:

$ cargo run
   Compiling struct v0.1.0 (file:///progetti/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:18
  |
3 |     nome_utente: &str,
  |                  ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Utente<'a> {
2 |     attivo: bool,
3 ~     nome_utente: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Utente<'a> {
2 |     attivo: bool,
3 |     nome_utente: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `struct` (bin "struct") due to 2 previous errors

Nel Capitolo 10, discuteremo come risolvere questi errori in modo da poter memorizzare reference nelle struct, ma per ora risolveremo errori come questi usando type con ownership come String invece di reference come &str.