Definizione e istanziazione di Struct
Le Struct sono simili ai tuple, discussi nella sezione “Il Tipo Tuple”, in quanto entrambi contengono più valori correlati. Come nei tuple, i componenti di una struct possono essere di tipi diversi. A differenza dei tuple, in una struct si assegna un nome a ciascun elemento di dati, quindi è chiaro cosa significano i valori. L'aggiunta di questi nomi rende le struct più flessibili dei tuple: non è necessario 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 di una struct dovrebbe descrivere l'importanza dei dati che vengono
raggruppati. Quindi, tra parentesi graffe, definiamo i nomi e i tipi dei dati, che
chiamiamo campi. Ad esempio, Elenco 5-1 mostra una struct che memorizza informazioni
su un account utente.
Nome del file: src/main.rs
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() {}
Per utilizzare una struct dopo averla definita, creiamo un' istanza di quella struct
specificando valori concreti per ciascuno dei campi. Creiamo un'istanza dichiarando
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 è necessario specificare i campi nello stesso ordine in cui sono stati dichiarati
nella struct. In altre parole, la definizione di struct è come un modello generale per il tipo,
e le istanze compilano quel modello con dati particolari per creare valori del tipo. Ad esempio,
possiamo dichiarare un utente specifico come mostrato nell'Elenco 5-2.
Nome del 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, }; }
Per ottenere un valore specifico da una struct, utilizziamo la notazione a punti. Ad esempio,
per accedere all'indirizzo email di questo utente, usiamo user1.email
. Se l'istanza è mutable,
possiamo cambiare un valore utilizzando la notazione a punti e assegnando a un campo particolare.
L'Elenco 5-3 mostra come cambiare il valore nel campo email
di un'istanza mutable di User
.
Nome del 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"); }
Nota che l'intera istanza deve essere mutable; Rust non permette di contrassegnare solo alcuni campi come mutable. Come con qualsiasi espressione, possiamo costruire una nuova istanza della struct come ultima espressione nel corpo della funzione per restituire implicitamente quella nuova istanza.
L'Elenco 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 true
, e il campo sign_in_count
ottiene un valore di 1
.
Nome del 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"), ); }
Ha senso chiamare 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 ogni nome diventerebbe ancora più fastidioso. Per fortuna,
esiste una scorciatoia comoda!
Utilizzo della scorciatoia per l'inizializzazione dei campi
Poiché i nomi dei parametri e i nomi dei campi della struct sono esattamente uguali in
l'Elenco 5-4, possiamo usare la sintassi scorciatoia per l'inizializzazione dei campi per
riscrivere build_user
in modo che si comporti esattamente allo stesso modo ma senza la ripetizione
di username
e email
, come mostrato nell'Elenco 5-5.
Nome del 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"), ); }
Qui, stiamo creando una nuova istanza della struct User
, che ha un campo denominato email
.
Vogliamo impostare il valore del campo email
al valore del parametro email
della funzione
build_user
. Poiché il campo email
e il parametro email
hanno lo stesso nome, dobbiamo
scrivere solo email
invece di email: email
.
Creazione di istanze da altre 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, ma ne cambia alcuni. È possibile farlo utilizzando la sintassi di aggiornamento delle struct.
Per prima cosa, nell'Elenco 5-6 mostriamo come creare una nuova istanza User
in user2
regolarmente, senza la sintassi di aggiornamento. Impostiamo un nuovo valore per email
ma
usiamo gli stessi valori di user1
che abbiamo creato nell'Elenco 5-2.
Nome del 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, }; }
Utilizzando la sintassi di aggiornamento delle struct, possiamo ottenere lo stesso effetto con
meno codice, come mostrato nell'Elenco 5-7. La sintassi ..
specifica che i campi rimanenti
non impostati esplicitamente devono avere lo stesso valore dei campi nell'istanza data.
Nome del 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 }; }
Il codice nell'Elenco 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 i campi rimanenti dovrebbero ottenere i loro valori dai
campi corrispondenti in user1
, ma possiamo scegliere di specificare valori per quanti campi
vogliamo in qualsiasi ordine, indipendentemente dall'ordine dei campi nella definizione della struct.
Nota che la sintassi di aggiornamento delle struct utilizza =
come un'assegnazione; questo
perché sposta i dati, proprio come abbiamo visto nella sezione “Variabili e Dati che Interagiscono
con il Movimento”. In questo esempio, non possiamo più utilizzare user1
come
un tutto dopo aver creato user2
perché il String
nel campo username
di user1
è stato
spostato in user2
. Se avessimo dato a user2
nuovi valori String
sia per email
che per
username
, e quindi avessimo utilizzato 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 trait Copy
, quindi il comportamento discusso nella sezione
“Dati Solo nel Pila: Copy” si applicherebbe.
Utilizzo di Struct a Tupla senza Campi Nominati per Creare Tipi Diversi
Rust supporta anche struct che sembrano simili ai tuple, chiamati struct a tupla. Le struct a tupla hanno il significato aggiunto fornito dal nome della struct ma non hanno nomi associati ai loro campi; piuttosto, hanno solo i tipi dei campi. Le struct a tupla sono utili quando si desidera dare all'intero tuple un nome e rendere il tuple un tipo diverso rispetto ad altri tuple, e quando assegnare un nome a ciascun campo come in una struct regolare sarebbe verboso o ridondante.
Per definire una struct a tupla, inizia con la parola chiave struct
e il nome della struct
seguiti dai tipi nel tuple. Ad esempio, qui definiamo e utilizziamo due struct a tupla chiamate
Color
e Point
:
Nome del 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); }
Nota che i valori black
e origin
sono tipi diversi perché sono istanze di struct a tupla
diverse. Ogni struct che definisci è il proprio tipo, anche se i campi all'interno della struct
potrebbero avere gli stessi tipi. Ad esempio, una funzione che prende un parametro di tipo Color
non può prendere un Point
come argomento, anche se entrambi i tipi sono costituiti da tre valori i32
.
Altrimenti, le istanze di struct a tupla sono simili ai tuple in quanto è possibile destrutturarle
nei loro singoli componenti, e si può utilizzare un .
seguito dall'indice per accedere a un valore
individuale.
Struct a Unità senza Campi
È possibile definire struct che non hanno campi! Questi sono chiamati struct a unità perché si
comportano in modo simile a ()
, il tipo unità che abbiamo menzionato nella sezione
“Il Tipo Tuple”. Le struct a unità possono essere utili quando è necessario
implementare un trait su qualche tipo ma non si hanno dati che si desidera memorizzare nel tipo stesso.
Discuteremo dei trait nel Capitolo 10. Ecco un esempio di dichiarazione e istanziazione di una struct
a unità chiamata AlwaysEqual
:
Nome del file: src/main.rs
struct AlwaysEqual; fn main() { let subject = AlwaysEqual; }
Per definire AlwaysEqual
, utilizziamo la parola chiave struct
, il nome che desideriamo,
e poi un punto e virgola. Nessun bisogno di parentesi graffe o parentesi! Poi possiamo ottenere
un'istanza di AlwaysEqual
nella variabile subject
in un modo simile: utilizzando il nome
che abbiamo definito, senza alcuna parentesi graffe 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, magari per avere un risultato noto per scopi di test. Non avremmo
bisogno di alcun dato per implementare quel comportamento! Vedrai nel Capitolo 10 come definire trait
e implementarli su qualsiasi tipo, incluse le struct a unità.
Proprietà dei Dati nelle Struct
Nella definizione di
User
nell'Elenco 5-1, abbiamo usato il tipoString
di proprietà piuttosto che il tipo di slice di stringa&str
. Questa è una scelta deliberata perché vogliamo che ciascuna istanza di questa struct sia proprietaria di tutti i suoi dati e che quei dati siano validi per tutto il tempo in cui la struct è valida.È anche possibile che le struct memorizzino riferimenti a dati posseduti da qualcos'altro, ma per farlo è necessario utilizzare le lifetime, una funzione di Rust che discuteremo nel Capitolo 10. Le lifetime 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 lifetime, come segue; questo non funzionerà:
Nome del 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à perché ha bisogno di specificatori 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, | For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` (bin "structs") due to 2 previous errors
Nel Capitolo 10, discuteremo come risolvere questi errori in modo che tu possa memorizzare riferimenti nelle struct, ma per ora, risolveremo errori come questi usando tipi di proprietà come
String
invece di riferimenti come&str
.