Trait in Rust
Un trait definisce un insieme di comportamenti che un tipo può implementare: un po’ come un’interfaccia in altri linguaggi. È il modo in cui Rust dice «questo tipo sa fare X», ed è ciò che sta dietro ai vincoli di trait che hai visto nella pagina sui generici.
Definire un trait
Sezione intitolata “Definire un trait”trait Speak { fn speak(&self) -> String;}Questo dice: qualsiasi tipo che implementa Speak deve fornire un metodo speak che restituisce una String. Da solo, un trait è solo una promessa: non fa niente finché un tipo non lo implementa.
Implementare un trait
Sezione intitolata “Implementare un trait”trait Speak { fn speak(&self) -> String;}
struct Dog;struct Cat;
impl Speak for Dog { fn speak(&self) -> String { String::from("Bau!") }}
impl Speak for Cat { fn speak(&self) -> String { String::from("Miao!") }}
fn main() { let dog = Dog; let cat = Cat;
println!("{}", dog.speak()); println!("{}", cat.speak());}Output:
Bau!Miao!Dog e Cat sono tipi che non hanno niente in comune, ma entrambi implementano Speak, quindi su entrambi puoi chiamare .speak(). È lo stesso schema dei blocchi impl della pagina sulle struct: impl Speak for Dog dice solo «ecco come Dog rispetta il trait Speak».
Trait come parametri di funzione
Sezione intitolata “Trait come parametri di funzione”Ed ecco il vero vantaggio: puoi scrivere una funzione che accetta qualsiasi tipo che implementa un trait, senza preoccuparti di quale tipo preciso sia:
trait Speak { fn speak(&self) -> String;}
struct Dog;impl Speak for Dog { fn speak(&self) -> String { String::from("Bau!") }}
fn announce(animal: &impl Speak) { println!("L'animale dice: {}", animal.speak());}
fn main() { let dog = Dog; announce(&dog);}animal: &impl Speak vuol dire «qualsiasi tipo che implementa Speak, non mi serve sapere quale». Aggiungi un tipo Bird che implementa anche lui Speak, e announce funziona anche con quello, senza cambiare niente.
Trait che hai già usato
Sezione intitolata “Trait che hai già usato”Ricordi #[derive(Debug)] dalla pagina sulle struct? Anche Debug è un trait: derive genera automaticamente l’implementazione al posto tuo, invece di fartela scrivere a mano. Altri trait comuni che incontrerai: Clone (per .clone()), PartialEq (per ==) e PartialOrd (per < e >, che hai visto usato come vincolo di trait nella pagina sui generici).
Implementazioni predefinite
Sezione intitolata “Implementazioni predefinite”Un trait può fornire il corpo predefinito di un metodo, che i tipi che lo implementano possono usare così com’è oppure sovrascrivere:
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Ciao, {}!", self.name()) }}Qualsiasi tipo che implementa Greet deve definire solo name(): greet() arriva gratis, a meno che il tipo non decida di sovrascrivere anche quello.
I trait hanno molto di più di quello che vedi in questa pagina. La pagina sui trait comuni presenta quelli standard che userai ogni giorno, e quella sui trait avanzati spiega i tipi associati, i supertrait e altri strumenti che vedrai nei progetti più grandi.
Pratica
Sezione intitolata “Pratica”1. Forme con un’area
Sezione intitolata “1. Forme con un’area”Definisci un trait Area con un solo metodo, fn area(&self) -> f64. Implementalo per un Rectangle (con width e height) e per un Square (con side). Stampa l’area di un rettangolo 3 per 4 e di un quadrato di lato 5. Suggerimento: ogni blocco impl Area for ... ha bisogno solo di quel metodo, e il suo corpo è una sola moltiplicazione.
Mostra la soluzione
trait Area { fn area(&self) -> f64;}
struct Rectangle { width: f64, height: f64,}
struct Square { side: f64,}
impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
impl Area for Square { fn area(&self) -> f64 { self.side * self.side }}
fn main() { let rect = Rectangle { width: 3.0, height: 4.0 }; let square = Square { side: 5.0 };
println!("Rettangolo: {}", rect.area()); println!("Quadrato: {}", square.area());}Output:
Rettangolo: 12Quadrato: 252. Una funzione per tanti tipi
Sezione intitolata “2. Una funzione per tanti tipi”Definisci un trait Describe con fn describe(&self) -> String. Implementalo per un Book (con un title) e per un Movie (con un title e minutes). Poi scrivi una sola funzione fn show(item: &impl Describe) che stampa la descrizione, e chiamala con entrambi. Suggerimento: usa format! per costruire ogni stringa di descrizione.
Mostra la soluzione
trait Describe { fn describe(&self) -> String;}
struct Book { title: String,}
struct Movie { title: String, minutes: u32,}
impl Describe for Book { fn describe(&self) -> String { format!("Libro: {}", self.title) }}
impl Describe for Movie { fn describe(&self) -> String { format!("Film: {} ({} min)", self.title, self.minutes) }}
fn show(item: &impl Describe) { println!("{}", item.describe());}
fn main() { let book = Book { title: String::from("Lo Hobbit") }; let movie = Movie { title: String::from("Up"), minutes: 96 };
show(&book); show(&movie);}Output:
Libro: Lo HobbitFilm: Up (96 min)3. Usa o sovrascrivi un metodo predefinito
Sezione intitolata “3. Usa o sovrascrivi un metodo predefinito”Prendi il trait Greet di questa pagina, con il suo metodo predefinito greet(). Implementalo per una Person, che tiene il comportamento predefinito, e per un Pirate, che sovrascrive greet() per dire invece Ahoy, ...!. Suggerimento: entrambi i tipi devono definire name(). Solo Pirate scrive anche il suo greet().
Mostra la soluzione
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Ciao, {}!", self.name()) }}
struct Person { name: String,}
struct Pirate { name: String,}
impl Greet for Person { fn name(&self) -> String { self.name.clone() }}
impl Greet for Pirate { fn name(&self) -> String { self.name.clone() }
fn greet(&self) -> String { format!("Ahoy, {}!", self.name()) }}
fn main() { let ana = Person { name: String::from("Ana") }; let jack = Pirate { name: String::from("Jack") };
println!("{}", ana.greet()); println!("{}", jack.greet());}Output:
Ciao, Ana!Ahoy, Jack!Riepilogo
Sezione intitolata “Riepilogo”- Un trait definisce un comportamento che un tipo può implementare, in modo simile a un’interfaccia.
impl NomeTrait for Tipofornisce l’implementazione.&impl NomeTraitcome tipo di un parametro accetta qualsiasi tipo che implementa quel trait.- I trait possono fornire corpi predefiniti per i metodi, che chi li implementa può usare o sovrascrivere.
Debug,CloneePartialEqsono trait comuni della libreria standard:#[derive(...)]li implementa automaticamente.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Cosa definisce un trait?
2. Cosa vuol dire animal: &impl Speak come tipo di un parametro?
3. Cosa fa davvero #[derive(Debug)]?
4. Cos'è un'implementazione predefinita in un trait?
5. Secondo questa pagina, quale di questi è un trait, e non una struct o un'enum?
Punteggio: 0 / 5
Per approfondire
Sezione intitolata “Per approfondire”- Traits: Defining Shared Behavior — The Rust Book (in inglese): il capitolo ufficiale su come definire i trait, i metodi predefiniti, i parametri
impl Traite i vincoli di trait. - Traits — Rust by Example (in inglese): un esempio eseguibile con un trait, un metodo predefinito e un’implementazione.
- Derive — Rust by Example (in inglese): quali trait standard
#[derive(...)]può implementare per te, con esempi. - Advanced Traits — The Rust Book (in inglese): il passo successivo, con tipi associati, supertrait e il pattern newtype.
- Traits — The Rust Reference (in inglese): le regole precise per definire i trait, per quando vuoi tutti i dettagli.