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.
struct Utente { attivo: bool, nome_utente: String, email: String, numero_accessi: u64, } fn main() {}
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.
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, }; }
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.
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"); }
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
.
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"), ); }
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.
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"), ); }
nuovo_utente
che usa la sintassi abbreviata perché i campi e i parametri nome_utente
e email
hanno lo stesso nomeQui 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.
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, }; }
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.
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 }; }
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
:
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
:
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à:
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
.