Los trait objects permiten trabajar con valores de tipos distintos a través de un interfaz común, sin saber en tiempo de compilación cuál es el tipo concreto. A diferencia de los genéricos, que generan código especializado para cada tipo (despacho estático), los trait objects usan una vtable para resolver la llamada en runtime (despacho dinámico).
Box<dyn Trait>: almacenar valores de tipos heterogéneos
El uso más habitual es Box<dyn Trait>. El Box coloca el valor en el heap y el puntero dyn Trait lleva asociada una vtable con las direcciones de los métodos implementados por ese tipo concreto.
trait Figura {
fn area(&self) -> f64;
fn nombre(&self) -> &str;
}
struct Circulo { radio: f64 }
struct Rectangulo { ancho: f64, alto: f64 }
impl Figura for Circulo {
fn area(&self) -> f64 { std::f64::consts::PI * self.radio * self.radio }
fn nombre(&self) -> &str { "Círculo" }
}
impl Figura for Rectangulo {
fn area(&self) -> f64 { self.ancho * self.alto }
fn nombre(&self) -> &str { "Rectángulo" }
}
fn area_total(figuras: &[Box<dyn Figura>]) -> f64 {
figuras.iter().map(|f| f.area()).sum()
}
fn main() {
let figuras: Vec<Box<dyn Figura>> = vec![
Box::new(Circulo { radio: 3.0 }),
Box::new(Rectangulo { ancho: 4.0, alto: 5.0 }),
Box::new(Circulo { radio: 1.5 }),
];
for f in &figuras {
println!("{}: {:.2}", f.nombre(), f.area());
}
println!("Total: {:.2}", area_total(&figuras));
}
Arc<dyn Trait + Send + Sync> para compartir entre hilos
Cuando necesitas compartir un trait object entre hilos, Box no es suficiente porque no es Send. Usa Arc y añade los bounds Send + Sync al trait object.
use std::sync::Arc;
use std::thread;
trait Procesador: Send + Sync {
fn procesar(&self, dato: u32) -> u32;
}
struct Doble;
struct Triple;
impl Procesador for Doble {
fn procesar(&self, dato: u32) -> u32 { dato * 2 }
}
impl Procesador for Triple {
fn procesar(&self, dato: u32) -> u32 { dato * 3 }
}
fn main() {
let proc: Arc<dyn Procesador> = Arc::new(Doble);
let handles: Vec<_> = (0..4).map(|i| {
let p = Arc::clone(&proc);
thread::spawn(move || p.procesar(i * 10))
}).collect();
for h in handles {
println!("{}", h.join().unwrap());
}
// 0, 20, 40, 60
}
Box<dyn Error> para errores universales
El patrón más extendido para manejar errores heterogéneos sin definir un tipo enum propio es devolver Box<dyn std::error::Error>. Funciona con cualquier tipo que implemente Error, incluidos los de bibliotecas externas.
use std::error::Error;
use std::num::ParseIntError;
use std::fmt;
#[derive(Debug)]
struct ErrorRango(i32);
impl fmt::Display for ErrorRango {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Valor {} fuera de rango [0, 100]", self.0)
}
}
impl Error for ErrorRango {}
fn parsear_porcentaje(s: &str) -> Result<i32, Box<dyn Error>> {
let n: i32 = s.trim().parse()?; // ParseIntError se convierte automáticamente
if n < 0 || n > 100 {
return Err(Box::new(ErrorRango(n)));
}
Ok(n)
}
fn main() {
for entrada in ["75", "abc", "150", "0"] {
match parsear_porcentaje(entrada) {
Ok(v) => println!("{entrada} ? {v}%"),
Err(e) => println!("{entrada} ? Error: {e}"),
}
}
}
Object safety: cuándo un trait no puede usarse como dyn
No todos los traits pueden convertirse en trait objects. Un trait es object-safe si cumple estas condiciones: ningún método devuelve Self, ningún método tiene parámetros genéricos, y el trait no requiere Sized. Si se viola alguna, el compilador emite E0038.
// ERROR E0038: Clone no es object-safe porque clone() devuelve Self
// fn clonar_figura(f: &Box<dyn Clone>) {}
// SOLUCIÓN: definir un trait propio sin Self en la firma
trait ClonableFigura {
fn clonar_boxed(&self) -> Box<dyn ClonableFigura>;
fn area(&self) -> f64;
}
#[derive(Clone)]
struct Cuadrado { lado: f64 }
impl ClonableFigura for Cuadrado {
fn clonar_boxed(&self) -> Box<dyn ClonableFigura> {
Box::new(self.clone())
}
fn area(&self) -> f64 { self.lado * self.lado }
}
fn duplicar(f: &dyn ClonableFigura) -> Box<dyn ClonableFigura> {
f.clonar_boxed()
}
fn main() {
let c = Cuadrado { lado: 4.0 };
let c2 = duplicar(&c);
println!("Original: {:.1}, Copia: {:.1}", c.area(), c2.area());
}
La regla práctica: si un trait incluye métodos que usan Self en la firma o parámetros de tipo, no puede usarse directamente como dyn Trait. La solución habitual es separar el comportamiento en un trait auxiliar o rediseñar la API para evitar Self en las firmas críticas.
