Traits avanzados en Rust: tipos asociados, implementaciones por defecto y trait objects

Los traits básicos de Rust —Display, Debug, Iterator— son suficientes para empezar. Pero conforme los proyectos crecen, necesitas patrones más potentes: tipos asociados que eliminan genéricos superfluos, implementaciones por defecto que reducen el boilerplate, supertraits para exigir capacidades compuestas y trait objects para polimorfismo dinámico controlado.

Tipos asociados: eliminar genéricos redundantes

En lugar de trait Conversor<T> donde T aparece en cada firma, los tipos asociados fijan el tipo de salida dentro del trait:

trait Conversor {
    type Salida;
    fn convertir(&self) -> Self::Salida;
}

struct Celsius(f64);
struct Fahrenheit(f64);

impl Conversor for Celsius {
    type Salida = Fahrenheit;
    fn convertir(&self) -> Fahrenheit {
        Fahrenheit(self.0 * 9.0 / 5.0 + 32.0)
    }
}

fn main() {
    let temp = Celsius(100.0);
    let f = temp.convertir();
    println!("{:.1}°F", f.0); // 212.0°F
}

Implementaciones por defecto

Los traits pueden ofrecer implementaciones por defecto que los tipos pueden sobreescribir o usar tal cual:

trait Resumen {
    fn autor(&self) -> String;

    // Implementación por defecto que reutiliza autor()
    fn resumen(&self) -> String {
        format!("Leer más de {}...", self.autor())
    }
}

struct Articulo {
    titulo: String,
    autor: String,
    cuerpo: String,
}

impl Resumen for Articulo {
    fn autor(&self) -> String { self.autor.clone() }

    // Sobreescribir el resumen
    fn resumen(&self) -> String {
        format!("{} — por {} — {:.50}", self.titulo, self.autor, self.cuerpo)
    }
}

struct Tweet {
    usuario: String,
    contenido: String,
}

impl Resumen for Tweet {
    fn autor(&self) -> String { format!("@{}", self.usuario) }
    // resumen() usa la implementación por defecto
}

fn main() {
    let t = Tweet { usuario: "rustacean".into(), contenido: "gran lenguaje".into() };
    println!("{}", t.resumen()); // "Leer más de @rustacean..."
}

Supertraits: composición de traits

use std::fmt;

// Cualquier tipo que implemente Imprimible debe implementar Display
trait Imprimible: fmt::Display {
    fn imprimir(&self) {
        println!("[OUTPUT] {self}");
    }
}

#[derive(Debug)]
struct Punto { x: f64, y: f64 }

impl fmt::Display for Punto {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

impl Imprimible for Punto {} // usa imprimir() por defecto

fn main() {
    let p = Punto { x: 1.0, y: 2.0 };
    p.imprimir(); // [OUTPUT] (1, 2)
}

Trait objects con dyn Trait

dyn Trait permite trabajar con distintos tipos que implementan el mismo trait sin conocer el tipo concreto en compilación. El coste es un puntero adicional (vtable) por despacho de método.

trait Forma {
    fn area(&self) -> f64;
    fn nombre(&self) -> &str;
}

struct Circulo { radio: f64 }
struct Rectangulo { ancho: f64, alto: f64 }

impl Forma for Circulo {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radio * self.radio }
    fn nombre(&self) -> &str { "círculo" }
}

impl Forma for Rectangulo {
    fn area(&self) -> f64 { self.ancho * self.alto }
    fn nombre(&self) -> &str { "rectángulo" }
}

fn area_total(formas: &[Box<dyn Forma>]) -> f64 {
    formas.iter().map(|f| f.area()).sum()
}

fn main() {
    let formas: Vec<Box<dyn Forma>> = 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 &formas {
        println!("{}: {:.2}", f.nombre(), f.area());
    }
    println!("Total: {:.2}", area_total(&formas));
}

impl Trait vs dyn Trait

// impl Trait en retorno: tipo concreto único, resuelto en compilación
fn crear_sumador(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

// dyn Trait en retorno: puede ser distintos tipos según lógica runtime
fn crear_forma(tipo: &str) -> Box<dyn Forma> {
    match tipo {
        "circulo"    => Box::new(Circulo { radio: 1.0 }),
        "rectangulo" => Box::new(Rectangulo { ancho: 2.0, alto: 3.0 }),
        _            => panic!("forma desconocida"),
    }
}

// Con impl Trait no puedes devolver distintos tipos en ramas diferentes
// Con dyn Trait sí puedes, a cambio de un puntero extra

Resumen

  • Los tipos asociados fijan tipos de salida dentro del trait y eliminan parámetros genéricos superfluos.
  • Las implementaciones por defecto reducen el boilerplate: los tipos implementan lo mínimo y heredan el resto.
  • Los supertraits exigen que el implementador cumpla otros traits, permitiendo llamar sus métodos en la implementación por defecto.
  • dyn Trait habilita polimorfismo dinámico: varias implementaciones distintas manejadas por el mismo código.
  • Usa impl Trait cuando el tipo de retorno siempre es el mismo; usa Box<dyn Trait> cuando puede variar en runtime.

COMPARTE ESTE ARTÍCULO

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