En esta página

Traits: abstracción y comportamiento compartido

15 min lectura TextoCap. 4 — Abstracciones

Traits: comportamiento compartido

Un trait define un conjunto de métodos que un tipo puede implementar. Son similares a las interfaces en otros lenguajes, pero más poderosos: permiten implementaciones por defecto, pueden tener bounds sobre tipos genéricos, y se usan extensamente en toda la biblioteca estándar.

Definir e implementar traits

trait Saludable {
    // Método que el tipo DEBE implementar
    fn saludar(&self) -> String;
    
    // Método con implementación por defecto
    fn despedirse(&self) -> String {
        format!("¡Hasta luego, {}!", self.nombre())
    }
    
    // Otro método abstracto
    fn nombre(&self) -> &str;
}

struct Persona {
    nombre: String,
    idioma: String,
}

struct Robot {
    id: u32,
    modelo: String,
}

impl Saludable for Persona {
    fn saludar(&self) -> String {
        match self.idioma.as_str() {
            "es" => format!("¡Hola! Soy {}", self.nombre),
            "en" => format!("Hello! I'm {}", self.nombre),
            _    => format!("Hi! I'm {}", self.nombre),
        }
    }
    
    fn nombre(&self) -> &str {
        &self.nombre
    }
}

impl Saludable for Robot {
    fn saludar(&self) -> String {
        format!("ROBOT-{} ({}) ONLINE. PROCESANDO...", self.id, self.modelo)
    }
    
    fn nombre(&self) -> &str {
        &self.modelo
    }
    
    // Sobrescribir el método por defecto
    fn despedirse(&self) -> String {
        format!("ROBOT-{} OFFLINE.", self.id)
    }
}

fn main() {
    let persona = Persona {
        nombre: String::from("Ana"),
        idioma: String::from("es"),
    };
    let robot = Robot { id: 42, modelo: String::from("T-800") };
    
    println!("{}", persona.saludar());
    println!("{}", persona.despedirse()); // Método por defecto
    
    println!("{}", robot.saludar());
    println!("{}", robot.despedirse()); // Implementación sobrescrita
}

Traits como parámetros

Existen dos sintaxis para usar traits en parámetros de función:

trait Area {
    fn area(&self) -> f64;
}

struct Cuadrado { lado: f64 }
struct Circulo { radio: f64 }

impl Area for Cuadrado {
    fn area(&self) -> f64 { self.lado * self.lado }
}

impl Area for Circulo {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radio * self.radio }
}

// Sintaxis 1: impl Trait (azúcar sintáctico, dispatch estático)
fn imprimir_area(forma: &impl Area) {
    println!("Área: {:.4}", forma.area());
}

// Sintaxis 2: trait bound explícito (más flexible con genéricos)
fn imprimir_area_generica<T: Area>(forma: &T) {
    println!("Área: {:.4}", forma.area());
}

// Con where clause (más legible cuando hay múltiples bounds)
fn comparar_areas<T>(forma1: &T, forma2: &T) -> bool
where
    T: Area,
{
    forma1.area() > forma2.area()
}

fn main() {
    let c = Cuadrado { lado: 4.0 };
    let r = Circulo { radio: 3.0 };
    
    imprimir_area(&c);
    imprimir_area_generica(&r);
    println!("¿Cuadrado tiene más área? {}", comparar_areas(&c, &r));
}

Múltiples trait bounds

use std::fmt::{Debug, Display};

fn imprimir_debug_y_display<T>(valor: &T)
where
    T: Debug + Display,
{
    println!("Debug: {:?}", valor);
    println!("Display: {}", valor);
}

fn comparar_y_mostrar<T>(a: T, b: T) -> T
where
    T: PartialOrd + Display + Clone,
{
    if a > b {
        println!("{a} > {b}");
        a
    } else {
        println!("{a} <= {b}");
        b.clone()
    }
}

fn main() {
    imprimir_debug_y_display(&42);
    imprimir_debug_y_display(&"hola");
    
    let mayor = comparar_y_mostrar(10, 20);
    println!("Mayor: {mayor}");
}

Supertraits

Un trait puede requerir que el tipo implemente otro trait (supertrait):

use std::fmt;

// Animal requiere que el tipo implemente fmt::Display
trait Animal: fmt::Display {
    fn sonido(&self) -> &str;
    fn patas(&self) -> u8;
    
    fn describir(&self) -> String {
        format!("{} hace '{}' y tiene {} patas", self, self.sonido(), self.patas())
    }
}

struct Perro { nombre: String }
struct Gato  { nombre: String }

impl fmt::Display for Perro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Perro({})", self.nombre)
    }
}

impl fmt::Display for Gato {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Gato({})", self.nombre)
    }
}

impl Animal for Perro {
    fn sonido(&self) -> &str { "guau" }
    fn patas(&self) -> u8 { 4 }
}

impl Animal for Gato {
    fn sonido(&self) -> &str { "miau" }
    fn patas(&self) -> u8 { 4 }
}

fn main() {
    let perro = Perro { nombre: String::from("Rex") };
    let gato  = Gato  { nombre: String::from("Whiskers") };
    
    println!("{}", perro.describir());
    println!("{}", gato.describir());
}

Implementar Display y Debug

Los traits Display y Debug son fundamentales para hacer que tus tipos sean imprimibles:

use std::fmt;

struct Temperatura {
    celsius: f64,
}

// Display: para output orientado al usuario
impl fmt::Display for Temperatura {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°C", self.celsius)
    }
}

// Debug: para output de depuración
impl fmt::Debug for Temperatura {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Temperatura")
            .field("celsius", &self.celsius)
            .field("fahrenheit", &(self.celsius * 1.8 + 32.0))
            .finish()
    }
}

fn main() {
    let t = Temperatura { celsius: 100.0 };
    println!("{t}");    // 100.0°C  (Display)
    println!("{t:?}");  // Debug format
    println!("{t:#?}"); // Debug pretty-print
}

Trait objects con dyn

Cuando necesitas una colección de tipos distintos que comparten un trait, usa trait objects:

use std::fmt;

trait Dibujable {
    fn dibujar(&self);
}

struct Circulo { radio: f64 }
struct Cuadrado { lado: f64 }
struct Triangulo { base: f64, altura: f64 }

impl Dibujable for Circulo {
    fn dibujar(&self) { println!("Dibujando círculo (r={})", self.radio); }
}
impl Dibujable for Cuadrado {
    fn dibujar(&self) { println!("Dibujando cuadrado (l={})", self.lado); }
}
impl Dibujable for Triangulo {
    fn dibujar(&self) { println!("Dibujando triángulo (b={}, h={})", self.base, self.altura); }
}

fn dibujar_todo(formas: &[Box<dyn Dibujable>]) {
    for forma in formas {
        forma.dibujar();
    }
}

fn main() {
    // Vec de tipos distintos que implementan Dibujable
    let lienzo: Vec<Box<dyn Dibujable>> = vec![
        Box::new(Circulo { radio: 5.0 }),
        Box::new(Cuadrado { lado: 3.0 }),
        Box::new(Triangulo { base: 4.0, altura: 6.0 }),
        Box::new(Circulo { radio: 2.5 }),
    ];
    
    dibujar_todo(&lienzo);
}

Los traits más importantes de std

Trait Propósito
Clone Copia explícita del valor
Copy Copia implícita (solo stack)
Debug Formateo para depuración {:?}
Display Formateo para usuarios {}
PartialEq / Eq Comparación de igualdad ==
PartialOrd / Ord Comparación de orden <, >
Hash Para usar como clave en HashMap
Iterator Protocolo de iteración
From / Into Conversiones entre tipos
Default Valor por defecto
Send / Sync Seguridad en concurrencia

Los traits son la base de todas las abstracciones en Rust. La próxima lección explora los generics — cómo escribir código que funciona con múltiples tipos sin duplicación.

impl Trait vs dyn Trait
impl Trait en parámetros o retornos usa dispatch estático (monomorphization) — el compilador genera código específico para cada tipo, máximo rendimiento. dyn Trait usa dispatch dinámico — un puntero gordo (fat pointer) que incluye una vtable, útil cuando no conoces el tipo en tiempo de compilación.
Trait objects con Box<dyn Trait>
Cuando necesitas colecciones de tipos distintos que implementan el mismo trait usa Box<dyn Trait>: let items: Vec<Box<dyn Describible>> = vec![Box::new(libro), Box::new(pelicula)]. Cada elemento puede ser de un tipo diferente.
rust
use std::fmt;

// Definir un trait
trait Describible {
    fn descripcion(&self) -> String;

    // Método con implementación por defecto
    fn descripcion_corta(&self) -> String {
        let d = self.descripcion();
        if d.len() > 50 {
            format!("{}...", &d[..47])
        } else {
            d
        }
    }
}

struct Libro {
    titulo: String,
    autor: String,
    paginas: u32,
}

struct Pelicula {
    titulo: String,
    director: String,
    duracion_min: u32,
}

impl Describible for Libro {
    fn descripcion(&self) -> String {
        format!("'{}' de {} ({} páginas)", self.titulo, self.autor, self.paginas)
    }
}

impl Describible for Pelicula {
    fn descripcion(&self) -> String {
        format!("'{}' dirigida por {} ({} min)", self.titulo, self.director, self.duracion_min)
    }
}

fn imprimir_descripcion(item: &impl Describible) {
    println!("{}", item.descripcion());
}

fn main() {
    let libro = Libro {
        titulo: String::from("El Señor de los Anillos"),
        autor: String::from("J.R.R. Tolkien"),
        paginas: 1178,
    };
    let pelicula = Pelicula {
        titulo: String::from("Inception"),
        director: String::from("Christopher Nolan"),
        duracion_min: 148,
    };

    imprimir_descripcion(&libro);
    imprimir_descripcion(&pelicula);
    println!("Corto: {}", libro.descripcion_corta());
}