wandres.dev
STRUCTS Y MÉTODOS · modelar datos

Derivar traits: Debug, Clone y el formato de depuración

El atributo derive, imprimir structs con el formato de depuración y su variante bonita, la diferencia entre Clone y Copy, y una primera mirada al zoo de traits derivables.

⏱ 17 min

Imprime un struct recién definido con {} y el compilador te frena: no sabe cómo mostrarlo. La solución no es escribir código de formato a mano, sino pedirle al compilador que lo genere con #[derive(...)]. Ese pequeño atributo es tu primera ventana a los traits, el mecanismo con el que Rust abstrae capacidades.

🎯 Al terminar esta lección sabrás
  • Usar #[derive(Debug)] para hacer un struct imprimible.
  • Formatear con {:?} y con la variante “bonita” {:#?}.
  • Distinguir Clone de Copy y cuándo entra en juego cada uno.
  • Reconocer el zoo de traits derivables y sus límites.

derive: el compilador escribe el impl

Un struct nuevo no sabe imprimirse. Esto no compila:

struct Punto { x: i32, y: i32 }

let p = Punto { x: 1, y: 2 };
println!("{:?}", p);   // error: Punto no implementa Debug

En vez de escribir la implementación a mano, la derivas:

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

let p = Punto { x: 1, y: 2 };
println!("{:?}", p);   // Punto { x: 1, y: 2 }

#[derive(Debug)] es un atributo que corre en compilación y genera la implementación del trait Debug por ti, campo a campo. No hay coste en tiempo de ejecución: el código generado es el que habrías escrito a mano.

Depurar con {:?} y {:#?}

Debug es para ti, el programador: un volcado estructural para inspeccionar y depurar. Tiene dos formatos:

#[derive(Debug)]
struct Config { host: String, puerto: u16, tls: bool }

let c = Config { host: String::from("localhost"), puerto: 8080, tls: true };

println!("{:?}", c);
// Config { host: "localhost", puerto: 8080, tls: true }

println!("{:#?}", c);
// Config {
//     host: "localhost",
//     puerto: 8080,
//     tls: true,
// }

{:?} es compacto; {:#?} (pretty) lo despliega en varias líneas, ideal para structs anidados. Ojo: {} (sin el ?) usa el trait Display, que no es derivable —lo veremos enseguida—.

Clone y Copy

Clone añade el método .clone(), que produce una copia profunda e independiente:

#[derive(Debug, Clone)]
struct Perfil { nombre: String, etiquetas: Vec<String> }

let a = Perfil { nombre: String::from("Ada"), etiquetas: vec![] };
let b = a.clone();      // copia explícita; 'a' sigue siendo válido

Clone es explícito a propósito: como copiar un String o un Vec reserva memoria, Rust te obliga a pedirlo con .clone() para que el coste sea visible. Su pariente Copy es para tipos triviales (enteros, bool, char) que se duplican copiando sus bits; con Copy, la asignación duplica en vez de mover, sin llamar a ningún código:

#[derive(Debug, Clone, Copy)]
struct Coord { x: i32, y: i32 }   // solo campos Copy, así que puede ser Copy

let p = Coord { x: 1, y: 2 };
let q = p;                        // COPIA implícita; 'p' sigue válido

Un tipo solo puede ser Copy si todos sus campos lo son: un struct con un String (que no es Copy) nunca podrá derivar Copy.

El zoo de traits derivables

Debug, Clone y Copy son la punta del iceberg. Los derivables estándar más frecuentes:

🐞

Debug

Formato de depuración con {:?} y {:#?}. Casi cualquier tipo debería derivarlo.

📋

Clone y Copy

Duplicar: Clone explícito y profundo; Copy implícito y trivial (solo bits).

⚖️

PartialEq y Eq

Comparar con ==. Eq marca igualdad total (no aplica a f64, que arrastra el NaN).

🗂️

Hash, Ord, Default

Hash para usarlo como clave de un mapa; Ord para ordenar; Default para un valor por defecto vía Default::default().

derive es una macro que compone: la capacidad viaja por los campos

#[derive(Clone)] no es magia: es una macro procedural que, en compilación, lee la definición del struct y escribe un impl Clone que clona cada campo. La consecuencia profunda es la composicionalidad estructural: el derive solo funciona si cada campo ya implementa el trait. Un struct Perfil { etiquetas: Vec<String> } puede derivar Clone porque Vec<String> es Clone; si un campo no lo fuera, la derivación fallaría, y el error señalaría exactamente ese campo. La capacidad viaja recursivamente por la estructura de tus tipos. Esto separa dos mundos: los traits estructurales (Debug, Clone, PartialEq, Hash) se pueden derivar mecánicamente porque su comportamiento se deduce campo a campo; los traits semánticos como Display —cómo se le muestra un valor a un humano— no se derivan, porque no hay respuesta mecánica a “¿cuál es la forma correcta de enseñar esto a una persona?”: esa decisión es tuya. Interioriza la distinción: cuando un trait describe forma, lo deriva el compilador; cuando describe significado, lo escribes tú. Declarar capacidades con derive en vez de programarlas a mano es la filosofía central de Rust, y la verás multiplicada cuando llegues a los traits en el nivel 13.

⚔️ Deriva y observa
  1. Define struct Vector3 { x: f64, y: f64, z: f64 } y deriva Debug. Imprímelo con {:?} y con {:#?}.
  2. Añade Clone y clona una instancia; modifica la copia y comprueba que el original no cambia.
  3. Añade Copy a Vector3 y observa que ahora la asignación ya no mueve. ¿Por qué se permite aquí y no con un campo String?
  4. Deriva PartialEq y compara dos vectores con ==.
  5. Intenta derivar Display y lee el error. Explica por qué Debug sí se deriva y Display no.