I trait sono il modo con cui Rust associa delle funzionalità a delle struct. Questi ultimi costrutti hanno la peculiare funzionalità di aggregare insieme alcune informazioni per andare a creare un'entità di livello superiore.
Come si definisce un trait in Rust
Istanziando un elemento basato su una struct si ottiene in Rust ciò che di più vicino c'è ad un oggetto di un qualsiasi linguaggio basato sulla Programmazione Orientata agli Oggetti. Ciò che manca sono le funzionalità definite per agire sui dati inseriti in una struct. Quello che - per portare avanti il parallelismo con la OOP - sono i metodi negli oggetti. I trait aggiungono proprio questo: funzioni associabili ad una struct che siano in grado di manipolare i dati inseriti al loro interno.
L'impiego di un trait si articola in tre fasi:
- definizione della struct. Ciò avverrà nel modo consueto specificando quali sono le informazioni che vogliamo annidare all'interno delle loro istanze;
- definizione del trait in cui inseriremo principalmente le signature delle singole funzioni;
- implementazione di un trait e associazione con la struct. Qui andremo a dare davvero un corpo alle funzioni la cui definizione è stata inserita nel trait.
Un esempio pratico
Mettiamo in pratica il tutto con un esempio. Immaginiamo una struct che definisca un generico studente che nel nostro progetto, supponiamo, sarà gestito solo con nominativo e voto:
struct Studente { voto: u8, nome: &'static str }
I valori che verranno incapsulati al suo interno sono una stringa per il nome e un intero senza segno a 8 bit. Definiamo alcune funzionalità che, al momento, sono del tutto scollegate dalla struct:
trait GestioneDati {
fn crea(nome: &'static str) -> Self;
fn nome(&self) -> &'static str;
fn promosso(&self) -> bool;
fn assegna_voto(&mut self, voto:u8);
Abbiamo definito una funzione crea
per la creazione di un nuovo oggetto, una sorta di getter ovvero nome
per il recupero del nominativo contenuto nell'oggetto senza alcuna manipolazione. promosso
svolge invece il ruolo di property dinamica in quanto valore prodotto all'atto dell'invocazione in base a dati posseduti dall'istanza. assegna_voto
permette infine di impostare il valore di una valutazione all'interno dell'istanza.
Mancano le implementazioni delle funzioni ma per il momento notiamo che ogni funzione riceve un parametro self
che userà come riferimento all'istanza stessa al fine di accedere ai membri interni della struct.
Implementazione dei metodi
Arriva ora il momento dell'implementazione dei metodi. Nel contempo provvederemo ad associare il trait alla struct che desideriamo usando il costrutto impl
...for
:
impl GestioneDati for Studente {
fn crea(nome: &'static str) -> Studente {
Studente { nome: nome, voto: 0 }
}
fn nome(&self) -> &'static str {
self.nome
}
fn promosso(&self) -> bool {
self.voto>=6
}
fn assegna_voto(&mut self, voto:u8) {
self.voto=voto
}
}
L'argomento self
non viene mai passato al momento dell'invocazione, ciò perché viene passato in maniera del tutto automatica. In pratica, se nel trait abbiamo specificato solo self
come argomento di una funzione, questa verrà invocata senza parametri.
Prova dell'implementazione in Rust
E' arrivato il momento di mettere il tutto al lavoro creando un'istanza e osservando il ruolo che le funzionalità descritte e associate hanno. Il main
che abbiamo predisposto per il test appare così:
fn main() {
let mut giovanni: Studente = GestioneDati::crea("Giovanni Neri");
giovanni.assegna_voto(3);
if giovanni.promosso() {
println!("Voto sufficiente, {} è stato promosso", giovanni.nome());
}
else {
println!("Purtoppo {} è stato bocciato", giovanni.nome());
}
}
Nel codice creiamo una nuova istanza della struct sfruttando il metodo crea
. Subito dopo assegniamo il voto che in questo caso non è sufficiente (consideriamo come soglia della sufficienza il 6).
Con if
controlliamo se lo studente è stato promosso. In base a questo stamperemo il messaggio che corrisponde a "Purtroppo Giovanni Neri è stato bocciato".
L'aspetto più interessante è che tutte le funzionalità associate alla struct possono essere invocate sulla singola istanza. Proprio come se fossero innestate in essa.
L'utilità dei trait consiste quindi in una sorta di strato operativo che sovrapponiamo alla struct desiderata. Pertanto nulla vieta di associare il medesimo trait a più struct.