Trait objects en Rust: dyn Trait, object safety, Box y despacho dinámico

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP