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.
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.
- Usar
#[derive(Debug)]para hacer un struct imprimible. - Formatear con
{:?}y con la variante “bonita”{:#?}. - Distinguir
ClonedeCopyy 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(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.
- Define
struct Vector3 { x: f64, y: f64, z: f64 }y derivaDebug. Imprímelo con{:?}y con{:#?}. - Añade
Cloney clona una instancia; modifica la copia y comprueba que el original no cambia. - Añade
CopyaVector3y observa que ahora la asignación ya no mueve. ¿Por qué se permite aquí y no con un campoString? - Deriva
PartialEqy compara dos vectores con==. - Intenta derivar
Displayy lee el error. Explica por quéDebugsí se deriva yDisplayno.