Debug y Display: imprimir para el programador y para el usuario
Dos traits para imprimir, dos públicos distintos. Debug es el volcado estructural para depurar y se deriva; Display es la representación pensada para el usuario y se escribe a mano. Por qué esa asimetría es una decisión de diseño, no un descuido.
Defines un struct, intentas imprimirlo con {} y el compilador te frena. Pruebas con {:?} y te pide derivar algo. No es un capricho: Rust tiene dos traits de impresión, y cada uno sirve a un lector distinto. Debug es el volcado que lees tú mientras depuras; Display es la cara que tu tipo enseña al usuario final. Uno se genera mecánicamente; el otro lo escribes tú. Entender por qué son dos y no uno es entender cómo Rust separa la forma del significado.
- Distinguir cuándo el formateo invoca a
Debugy cuándo aDisplay. - Derivar
Debugy usar{:?}junto a su variante bonita{:#?}. - Implementar
Displaya mano conFormattery la macrowrite!. - Razonar por qué
Debuges derivable yDisplayno puede serlo.
Un valor, dos lectores
Todo valor de tu programa lo leen dos públicos con necesidades opuestas. El programador quiere ver la estructura interna para depurar: qué campos hay y qué contienen, sin adornos. El usuario final quiere una representación pensada para él, donde los detalles internos sobran y solo importa el significado. Rust se niega a servir a ambos con la misma función, y por eso reparte el trabajo en dos traits: Debug para el primero, Display para el segundo.
La marca de formato elige el trait. {:?} (y su hermana {:#?}) invoca a Debug; {} invoca a Display. No son intercambiables: un tipo puede implementar uno, el otro, ambos o ninguno, y la marca que uses exige exactamente el trait correspondiente.
Debug: el volcado del programador
Debug es para ti. Su formato reproduce la estructura del valor campo a campo, y casi nunca querrás escribirlo a mano: lo derivas.
#[derive(Debug)]
struct Usuario {
nombre: String,
edad: u8,
activo: bool,
}
fn main() {
let u = Usuario { nombre: String::from("Ada"), edad: 36, activo: true };
println!("{u:?}"); // Usuario { nombre: "Ada", edad: 36, activo: true }
println!("{u:#?}"); // el mismo volcado, desplegado en varias lineas
}
{:?} es compacto; {:#?} (pretty) lo despliega indentado, ideal para structs anidados. La regla práctica es simple: casi cualquier tipo debería derivar Debug. Es tan barato como añadirlo a la lista del derive, y sin él ni siquiera puedes usar assert_eq! o inspeccionar un valor en un test fallido.
Hay un matiz que se olvida: el formato de Debug no es un contrato estable. La biblioteca estándar se reserva el derecho de cambiar cómo se ven sus tipos entre versiones. No parsees la salida de {:?}; es para leerla un humano depurando, no para que la consuma otro programa.
Casi siempre derivas Debug, pero a veces conviene escribirlo. El caso clásico es un campo sensible —una contraseña, un token de sesión— que no quieres ver impreso en un log de depuración. Implementas Debug a mano y te apoyas en el ayudante f.debug_struct, que reproduce el formato del derive pero te deja decidir qué muestras:
use std::fmt;
struct Credencial {
usuario: String,
token: String,
}
impl fmt::Debug for Credencial {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Credencial")
.field("usuario", &self.usuario)
.field("token", &"[REDACTADO]")
.finish()
}
}Sigue siendo Debug —responde a {:?} igual que el derivado—, pero ahora tú gobiernas qué estructura se revela y qué se oculta.
Display: la voz del tipo
Display es para el usuario. No se deriva: lo implementas tú porque solo tú sabes cómo debe presentarse tu tipo.
use std::fmt;
struct Dinero {
centavos: i64,
}
impl fmt::Display for Dinero {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let signo = if self.centavos < 0 { "-" } else { "" };
let abs = self.centavos.abs();
write!(f, "{signo}{}.{:02} EUR", abs / 100, abs % 100)
}
}
fn main() {
let precio = Dinero { centavos: 1299 };
println!("{precio}"); // 12.99 EUR
let etiqueta: String = precio.to_string(); // ToString gratis
}
El método fmt recibe un Formatter, un búfer al que empujas texto con write!, y devuelve fmt::Result. Fíjate en el regalo: implementar Display te da ToString gratis. El método .to_string() existe para todo tipo Display mediante una implementación general (impl<T: Display> ToString for T), y {} es la marca que usan println!, format! y compañía para producir la cadena orientada al usuario.
Un Display escrito con write! ignora los flags de anchura y alineación que pase quien lo llama, como {:>10} o {:^8}. Esos flags viven en el Formatter, y solo se aplican si tu implementación los consulta o, más simple, si construyes la cadena y la pasas por f.pad(&s) en vez de por write!. Para presentación sencilla write! basta; para un tipo que quieres que se alinee como un &str en tablas, usa f.pad.
Por qué Display no se deriva
Aquí está el corazón del nivel. Debug se puede derivar porque “muestra la estructura” tiene una respuesta obvia y mecánica: escribe el nombre del tipo y lista sus campos con sus nombres. Un algoritmo lo resuelve sin ambigüedad, y eso es justo lo que hace la macro del derive.
Display no se puede derivar porque “muéstraselo a un humano” no tiene respuesta mecánica. ¿Un Dinero { centavos: 1299 } se enseña como 12.99 EUR, como $12.99, como 1299 centavos o redondeado a 13 EUR? No existe un algoritmo que lo decida: es una decisión de producto, de idioma, de contexto. Solo el autor del tipo la conoce, así que el lenguaje se la exige a él.
Debug — forma
Volcado estructural con {:?} y {:#?}. Derivable, mecánico, para el programador. Formato NO estable: no lo parsees.
Display — significado
Representación para el usuario con {}. Se escribe a mano, es una decisión semántica, y regala ToString.
flowchart TD V[Un valor] --> M[Que marca de formato usas] M -->|interrogacion| D[Invoca Debug] M -->|llaves vacias| P[Invoca Display] D --> DF[Estructura mecanica derivable para el programador] P --> PF[Significado escrito a mano para el usuario] style D fill:#f38ba8,color:#11111b style P fill:#a6e3a1,color:#11111b style DF fill:#89b4fa,color:#11111b style PF fill:#cba6f7,color:#11111b
La separación entre Debug y Display parece un detalle de la biblioteca de formato, pero es una tesis sobre la naturaleza de los datos. Cada valor de tu programa es leído por dos entidades con intereses incompatibles: la máquina y el programador, que necesitan ver la estructura —qué hay dentro, para inspeccionarlo y depurarlo—, y el usuario humano, que necesita el significado —una representación diseñada, donde los detalles internos son ruido—. Debug proyecta la estructura: es mecánico, derivable y explícitamente inestable, porque su público entiende que está mirando las tripas y que estas pueden cambiar. Display proyecta el significado: no es derivable, porque no hay respuesta mecánica a “¿cómo se le enseña esto a una persona?”, y suele ser un contrato estable, porque su público espera consistencia. Esta bifurcación se repite en todo Rust: serde::Serialize es una tercera proyección, la que va por el cable hacia otra máquina, y también se distingue de las otras dos. Los lenguajes que colapsan todo en un único toString() acaban obligándote a parsear en producción una salida de depuración pensada para leerse con los ojos, o a filtrar al usuario la estructura interna de tus tipos. Rust codifica en el sistema de tipos una negativa: no fingir que una sola representación sirve a todos los lectores. Cuando el compilador te exige elegir entre {:?} y {}, te está obligando a contestar una pregunta que siempre existió y que otros lenguajes te dejaban ignorar: ¿para quién es esta impresión?
{:?} usa Debug (derivable, estructural, para el programador, formato no estable); {} usa Display (escrito a mano, semántico, para el usuario). Deriva Debug en casi todo. Implementa Display solo cuando tu tipo tenga una representación humana con sentido, y recibirás ToString de regalo. Debug describe forma; Display describe significado: por eso uno lo genera el compilador y el otro lo decides tú.
- Define
struct Coordenada { lat: f64, lon: f64 }, derivaDebuge imprímela con{:?}y{:#?}. - Implementa
Displaypara que se vea como41.40 N, 2.17 E(elige tú el redondeo y los signos). Imprímela con{}. - Llama a
.to_string()sobre unaCoordenaday explica de dónde sale ese método sin haberlo escrito. - Intenta derivar
Displaycon#[derive(Display)]y lee el error del compilador. Explica por quéDebugsí se deriva yDisplayno. - Imprime una
Coordenadacon{:>25}y observa si tuDisplayrespeta la anchura. Si no lo hace, reescríbelo usandof.padsobre una cadena construida conformat!.