Strutture
Come ogni linguaggio comune, Rust supporta l'aggregazione di valori in strutture e la definizione di metodi che operano sui dati contenuti nella struttura.
Le strutture possono essere considerate come delle tuple, dove però vengono definiti dei nomi ai diversi campi che la compongono.
Per definire una struttura, viene usata la parola chiave struct, seguita da una lista di campi insieme ai loro tipi.
Questo è un esempio di come si potrebbe definire una struttura per contenere dati di un account.
#![allow(unused)] fn main() { struct Account { username: String, email: String, is_admin: bool, login_count: u64, } }
Possiamo adesso creare una istanza di questa struttura specificando i valori per ogni campo:
struct Account { username: String, email: String, is_admin: bool, login_count: u64, } fn main() { let admin = Account { username: String::from("admin"), email: String::from("admin@example.com"), is_admin: true, login_count: 0, }; }
Possiamo ovviamente creare delle funzioni che restituiscono delle strutture.
struct Account { username: String, email: String, is_admin: bool, login_count: u64, } fn build_account(username: String, email: String) -> Account { Account { username: username, email: email, is_admin: true, login_count: 0, } } fn main() { let admin = build_account(String::from("admin"), String::from("admin@example.com")); }
In questo caso, non prendiamo in ingresso delle reference a stringhe nella funzione build_account, perché vogliamo prendere ownership delle stringhe in ingresso.
L'ownership delle stringhe sarà poi restituita al chiamante all'interno della struttura, nei due campi username e email.
In generale, la variabile che ha ownership di una struttura detiene anche l'ownership di tutti i suoi campi, e se una struttura è dichiarata mutabile allora anche tutti i suoi campi lo sono, mentre se è dichiarata immutabile anche tutti i suoi campi lo sono.
Stampa delle strutture e debug
Una cosa molto importante sarebbe scrivere in output il valore dei campi della struttura, ma non è sufficiente usare la solita macro println! come abbiamo fatto fino ad adesso: infatti se proviamo a stampare una struttura
struct Account { username: String, email: String, is_admin: bool, login_count: u64, } fn build_account(username: String, email: String) -> Account { Account { username: username, email: email, is_admin: true, login_count: 0, } } fn main() { let admin = build_account( String::from("admin"), String::from("admin@example.com"), ); println!("admin: {admin}"); }
Il compilatore ci restituisce un errore perché la il tipo Account non implementa std::fmt::Display, che è un trait che specifica come devono essere formattati gli oggetti quando si provano a stampare.
Una situazione analoga potrebbe essere la definizione di del metodo toString() in Java, anche se in questo caso la semantica è leggermente diversa.
error[E0277]: `Account` doesn't implement `std::fmt::Display`
Quello che possiamo fare, però, è usare un formattatore di debug come ci suggerisce il compilatore
= help: the trait `std::fmt::Display` is not implemented for `Account`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Possiamo quindi cambiare il formattatore con quello di debug, e abilitare l'opzione per la struttura Account per essere stampabile come debug.
Questo infatti non viene abilitato di default, ma deve essere attivato derivando il trait Debug.
Vedremo meglio i trait nel prossimo capitolo, ma per adesso possiamo stampare la struttura in questo modo:
#[derive(Debug)] struct Account { username: String, email: String, is_admin: bool, login_count: u64, } // ... fn build_account(username: String, email: String) -> Account { Account { username: username, email: email, is_admin: true, login_count: 0, } } fn main() { let admin = build_account( String::from("admin"), String::from("admin@example.com"), ); println!("admin: {:#?}", admin); }
Eseguendo il programma vedremo stampati tutti i campi della nostra struttura, formattati in modo ordinato.
admin: Account {
username: "admin",
email: "admin@example.com",
is_admin: true,
login_count: 0,
}
Bisogna notare che questa feature è strettamente intesa per stampare informazioni di debug, per una serializzazione più robusta ci si può affidare a librerie specifiche come serde.
Metodi
Possiamo definire delle funzioni associate a una struttura tramite dei blocchi impl (implementation):
struct Vector { x: f32, y: f32 } impl Vector { fn length(&self) -> f32 { (self.x*self.x + self.y*self.y).sqrt() } } fn main() { let v = Vector { x: 2.0, y: 3.0 }; println!("La lughezza è {}", v.length()); }
l'output del programma è
La lughezza è 55.31727
Per definire una funzione nel contesto della struttura Vector iniziamo un blocco impl.
Possiamo definire una funzione length che prende in ingresso una reference a self.
In realtà questo è zucchero sintattico equivalente a length(self: &Self) e Self è un alias al tipo di cui stiamo definendo l'implementazione (in questo caso Rectangle).
Il primo argomento è quindi l'oggetto su cui stiamo chiamando il metodo.
Possiamo scegliere se prendere in ingresso una reference immutabile con &self, una reference mutabile con &mut self, oppure prendere direttamente ownership dell'oggetto con self.
Nell'esempio abbiamo scelto &self perché non vogliamo prendere ownership dell'oggetto, in quanto si dovrebbe poi restituire al chiamante, in maniera simile a come è stato già descritto nel capitolo sulle funzioni.
Possiamo poi chiamare il metodo tramite una sintassi particolare, ovvero oggetto.metodo(argomenti).
Questa chiamata popolerà il primo argomento con l'oggetto su cui stiamo invocando il metodo.
Funzioni associate
Non è necessario per una funzione dentro un blocco impl di prendere come primo argomento un oggetto.
Possiamo anche definire delle funzioni associate a una struttura, ma che possono essere chiamate senza avere una istanza della struttura.
Un esempio di tali funzioni è String::from che abbiamo usato in precedenza.
Per definire una funzione del genere semplicemente possiamo omettere il primo parametro self.
struct Vector { x: f32, y: f32 } impl Vector { fn zero() -> Self { Vector { x: 0.0, y: 0.0 } } fn length(&self) -> f32 { (self.x*self.x + self.y*self.y).sqrt() } } fn main() { let v = Vector::zero(); println!("La lughezza è {}", v.length()); }
La funzione può essere chiamata tramite la sintassi Struttura::funzione(argomenti), e si comporta come una qualunque funzione.
L'unica differenza è che viene definita nel contesto della struttura, quindi è possibile utilizzare ad esempio l'alias di tipo Self.