Definire e istanziare le Structs

Le Structs sono simili alle tuple, discusse nella sezione “Il tipo Tuple”, in quanto entrambe contengono valori correlati multipli. Come le tuple, i pezzi di una struct possono essere di tipi diversi. A differenza delle tuple, in una struct nominerai ciascun pezzo di dati in modo che sia chiaro cosa significano i valori. Aggiungere questi nomi significa che le structs 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 nominiamo l'intera struct. Il nome della struct dovrebbe descrivere l'importanza dei pezzi di dati raggruppati insieme. Poi, tra parentesi graffe, definiamo i nomi e i tipi dei pezzi di dati, che chiamiamo campi. Ad esempio, il Listing 5-1 mostra una struct che memorizza informazioni su un account utente.

Nome file: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}

Listing 5-1: Una definizione di struct User

Per utilizzare una struct dopo averla definita, creiamo un' istanza di quella struct specificando valori concreti per ciascun campo. Creiamo un'istanza indicando il nome della struct e poi aggiungiamo 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 tipo, e le istanze riempiono quel modello con dati particolari per creare valori del tipo. Ad esempio, possiamo dichiarare un particolare utente come mostrato nel Listing 5-2.

Nome file: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}

Listing 5-2: Creare un'istanza della struct User

Per ottenere un valore specifico da una struct, usiamo la notazione a punti. Ad esempio, per accedere all'indirizzo email di questo utente, usiamo user1.email. Se l'istanza è mutable, possiamo modificare un valore usando la notazione a punti e assegnando a un campo particolare. Il Listing 5-3 mostra come modificare il valore nel campo email di un'istanza User mutabile.

Nome file: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}

Listing 5-3: Cambiare il valore nel campo email di un istanza User

Si noti che l'intera istanza deve essere mutable; Rust non ci permette di indicare solo certi campi come mutable. Come per ogni espressione, possiamo costruire una nuova istanza della struct come l'ultima espressione nel blocco della funzione per restituire implicitamente quella nuova istanza.

Il Listing 5-4 mostra una funzione build_user che restituisce un'istanza User con l'email e il nome utente forniti. Il campo active ottiene il valore di true, e il sign_in_count ottiene un valore di 1.

Nome file: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Listing 5-4: Una funzione build_user che prende un'email e un nome utente e restituisce un'istanza User

Ha senso denominare i parametri della funzione con lo stesso nome dei campi della struct, ma dover ripetere i nomi dei campi email e username e le variabili è un po' tedioso. Se la struct avesse più campi, ripetere ciascun nome sarebbe ancora più fastidioso. Fortunatamente, c'è una scorciatoia comoda!

Usare la Scorciatoia di Inizializzazione dei Campi

Poiché i nomi dei parametri e i nomi dei campi della struct sono esattamente gli stessi nel Listing 5-4, possiamo usare la sintassi della scorciatoia di inizializzazione dei campi per riscrivere build_user in modo che si comporti esattamente nello stesso modo ma senza la ripetizione di username e email, come mostrato nel Listing 5-5.

Nome file: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Listing 5-5: Una funzione build_user che usa la scorciatoia di inizializzazione dei campi poiché i parametri username e email hanno lo stesso nome dei campi della struct

Qui, stiamo creando una nuova istanza della struct User, che ha un campo denominato email. Vogliamo impostare il valore del campo email sul valore del parametro email della funzione build_user. Poiché il campo email e il parametro email hanno lo stesso nome, dobbiamo solo scrivere email anziché email: email.

Creare Istanza da Altre Istanze con la Sintassi di Aggiornamento delle Struct

Spesso è utile creare una nuova istanza di una struct che includa la maggior parte dei valori da un'altra istanza, ma cambi alcuni. Puoi farlo usando la sintassi di aggiornamento della struct.

Per prima cosa, nel Listing 5-6 mostriamo come creare una nuova istanza User in user2 regularmente, senza la sintassi di aggiornamento. Impostiamo un nuovo valore per email ma utilizziamo gli stessi valori di user1 che abbiamo creato nel Listing 5-2.

Nome file: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

Listing 5-6: Creare una nuova istanza User utilizzando tutti i valori meno uno da user1

Usando la sintassi di aggiornamento della struct, possiamo ottenere lo stesso effetto con meno codice, come mostrato nel Listing 5-7. La sintassi .. specifica che i campi rimanenti non esplicitamente impostati dovrebbero avere lo stesso valore dei campi nell'istanza data.

Nome file: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

Listing 5-7: Usare la sintassi di aggiornamento delle struct per impostare un nuovo valore email per un'istanza User ma usare il resto dei valori da user1

Il codice nel Listing 5-7 crea anche un'istanza in user2 che ha un valore diverso per email ma ha gli stessi valori per i campi username, active e sign_in_count da user1. Il ..user1 deve venire per ultimo per specificare che tutti i campi rimanenti devono ottenere i loro valori dai campi corrispondenti in user1, ma possiamo scegliere di specificare i valori per quanti campi vogliamo in qualsiasi ordine, indipendentemente dall'ordine dei campi nella definizione della struct.

Si noti che la sintassi di aggiornamento delle struct utilizza = come un'assegnazione; questo è perché permette di muovere i dati, proprio come abbiamo visto nella sezione “Variabili e Dati che Interagiscono con il Move”. In questo esempio, non possiamo più usare user1 nella sua interezza dopo aver creato user2 perché la String nel campo username di user1 è stata spostata in user2. Se avessimo dato a user2 nuovi valori String per entrambi email e username, e quindi usato solo i valori active e sign_in_count da user1, allora user1 sarebbe ancora valido dopo aver creato user2. Sia active che sign_in_count sono tipi che implementano il Copy trait, quindi il comportamento che abbiamo discusso nella sezione “Dati Solo Stack: Copy” si applicherebbe.

Usare le Tuple Structs Senza Nomi di Campi per Creare Tipi Diversi

Rust supporta anche le structs che sembrano simili alle tuple, chiamate tuple structs. Le Tuple structs hanno in più il significato fornito dal nome della struct ma non hanno nomi associati ai loro campi; piuttosto, hanno solo i tipi dei campi. Le Tuple structs sono utili quando si vuole dare un nome all'intera tupla e rendere la tupla un tipo diverso dalle altre tuple, e quando nominare ciascun campo come in una struct regolare sarebbe ingombrante o ridondante.

Per definire una tuple struct, iniziare con la parola chiave struct e il nome della struct seguiti dai tipi nella tupla. Ad esempio, qui definiamo e usiamo due tuple structs chiamate Color e Point:

Nome file: src/main.rs

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Si noti che i valori black e origin sono di tipi diversi perché sono istanze di tuple structs diverse. Ogni struct che definisci è il proprio tipo, anche se i campi all'interno della struct possono avere gli stessi tipi. Ad esempio, una funzione che accetta un parametro di tipo Color non può accettare un Point come argomento, anche se entrambi i tipi sono costituiti da tre valori i32. In caso contrario, le istanze delle tuple structs sono simili alle tuple in quanto puoi destrutturarle nei loro singoli pezzi, e puoi usare un . seguito dall'indice per accedere a un valore individuale.

Structs Simili a Unit Senza Alcun Campo

Puoi anche definire structs che non hanno alcun campo! Queste si chiamano struct simili a unit perché si comportano in modo simile a (), il tipo unit menzionato nella sezione “Il tipo Tuple”. Le structs simili a unit possono essere utili quando hai bisogno di implementare un trait su qualche tipo ma non hai alcun dato che vuoi memorizzare nel tipo stesso. Discuteremo i traits nel Capitolo 10. Ecco un esempio di dichiarazione e istanziazione di una struct unit chiamata AlwaysEqual:

Nome file: src/main.rs

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Per definire AlwaysEqual, usiamo la parola chiave struct, il nome che vogliamo, e poi un punto e virgola. Non c'è bisogno di parentesi graffe o parentesi! Poi possiamo ottenere un istanza di AlwaysEqual nella variabile subject in modo simile: usando il nome che abbiamo definito, senza alcuna parentesi graffa o parentesi. Immagina che più tardi implementeremo un comportamento per questo tipo tale che ogni istanza di AlwaysEqual sia sempre uguale a ogni istanza di qualsiasi altro tipo, forse per avere un risultato noto a fini di test. Non avremmo bisogno di alcun dato per implementare quel comportamento! Nel Capitolo 10 vedrai come definire i traits e implementarli su qualsiasi tipo, incluse le structs simili a unit.

Proprietà dei Dati della Struct

Nella definizione della struct User nel Listing 5-1, abbiamo usato il tipo String posseduto piuttosto che il tipo &str string slice. Questa è una scelta deliberata perché vogliamo che ogni istanza di questa struct possieda tutti i propri dati e che quei dati siano validi per tutto il tempo in cui l'intera struct è valida.

È anche possibile per le structs memorizzare riferimenti a dati posseduti da qualcos'altro, ma per farlo è necessario l'uso delle lifetimes, una caratteristica di Rust di cui parleremo nel Capitolo 10. Le Lifetimes assicurano che i dati riferiti da una struct siano validi per tutto il tempo in cui la struct è valida. Supponiamo che tu provi a memorizzare un riferimento in una struct senza specificare le lifetimes, come il seguente; questo non funzionerà:

Nome file: src/main.rs

struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: "someusername123",
        email: "someone@example.com",
        sign_in_count: 1,
    };
}

Il compilatore si lamenterà che sono necessari dei specifiers di lifetime:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'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 User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

Per ulteriori informazioni su questo errore, prova `rustc --explain E0106`.
error: impossibile compilare `structs` (bin "structs") a causa di 2 errori precedenti

Nel Capitolo 10, discuteremo come risolvere questi errori in modo da poter memorizzare riferimenti in structs, ma per ora, correggeremo errori come questi utilizzando tipi posseduti come String anziché riferimenti come &str.