Trait Objects in Rust
I trait objects in Rust permettono il polimorfismo dinamico: lavorare con tipi diversi che implementano lo stesso trait, decidendo il metodo da chiamare a runtime.
Dispatch Statico vs Dinamico
trait Animale {
fn verso(&self) -> &str;
}
struct Cane;
struct Gatto;
impl Animale for Cane {
fn verso(&self) -> &str { "Bau!" }
}
impl Animale for Gatto {
fn verso(&self) -> &str { "Miao!" }
}
// Dispatch STATICO (generics) - risolto a compile-time
fn stampa_verso_statico(a: &impl Animale) {
println!("{}", a.verso());
}
// Dispatch DINAMICO (trait object) - risolto a runtime
fn stampa_verso_dinamico(a: &dyn Animale) {
println!("{}", a.verso());
}
Sintassi dyn Trait
I trait objects si creano con dyn NomeTrait e devono stare dietro un puntatore:
fn main() {
let cane = Cane;
let gatto = Gatto;
// Riferimento a trait object
let animale: &dyn Animale = &cane;
println!("{}", animale.verso());
// Box<dyn Trait> - ownership del trait object
let animale: Box<dyn Animale> = Box::new(Gatto);
println!("{}", animale.verso());
}
Vec di Trait Objects
Permette di avere una collezione di tipi diversi:
fn main() {
let animali: Vec<Box<dyn Animale>> = vec![
Box::new(Cane),
Box::new(Gatto),
Box::new(Cane),
];
for animale in &animali {
println!("{}", animale.verso());
}
// Bau! Miao! Bau!
}
Object Safety
Non tutti i trait possono diventare trait objects. Un trait è object-safe se:
- I metodi non restituiscono
Self - I metodi non hanno parametri di tipo generico
// Object-safe ✅
trait Disegnabile {
fn disegna(&self);
fn area(&self) -> f64;
}
// NON object-safe ❌
trait Clonabile {
fn clona(&self) -> Self; // Restituisce Self
}
trait Comparabile {
fn confronta<T>(&self, other: &T); // Parametro generico
}
Pattern: Strategia con Trait Objects
trait Formatter {
fn formatta(&self, dati: &str) -> String;
}
struct FormatterJSON;
struct FormatterXML;
impl Formatter for FormatterJSON {
fn formatta(&self, dati: &str) -> String {
format!("{{\"dati\": \"{}\"}}", dati)
}
}
impl Formatter for FormatterXML {
fn formatta(&self, dati: &str) -> String {
format!("<dati>{}</dati>", dati)
}
}
fn esporta(formatter: &dyn Formatter, dati: &str) -> String {
formatter.formatta(dati)
}
fn main() {
let json = FormatterJSON;
let xml = FormatterXML;
println!("{}", esporta(&json, "ciao")); // {"dati": "ciao"}
println!("{}", esporta(&xml, "ciao")); // <dati>ciao</dati>
}
Trait Objects con Lifetime
trait Logger {
fn log(&self, messaggio: &str);
}
// Con lifetime esplicita
fn crea_logger<'a>(livello: &'a str) -> Box<dyn Logger + 'a> {
// ...
todo!()
}
Quando Usare Trait Objects vs Generics
| Criterio | Generics | Trait Objects |
|---|---|---|
| Performance | Zero-cost | Overhead vtable |
| Tipi eterogenei | No | Sì |
| Dimensione binario | Più grande (monomorphization) | Più piccolo |
| Flessibilità runtime | No | Sì |
Conclusione
I trait objects offrono polimorfismo dinamico in Rust tramite dyn Trait. Usali quando hai bisogno di lavorare con tipi eterogenei a runtime (collezioni miste, plugin, strategie). Preferisci i generics quando la performance è critica e i tipi sono noti a compile-time. La regola di object safety garantisce che il compilatore possa costruire la vtable necessaria per il dispatch dinamico.