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 Traithabilita polimorfismo dinámico: varias implementaciones distintas manejadas por el mismo código.- Usa
impl Traitcuando el tipo de retorno siempre es el mismo; usaBox<dyn Trait>cuando puede variar en runtime.
