3.5 Voz propia
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
Firmar una obligación no es el último paso. Un artesano que puede hacer piezas pero no puede describirlas trabaja para un solo público. Un informe técnico es preciso y completo - para quien lo compiló. Esto suele llamarse precisión. La palabra exacta es la mitad del trabajo.
El intento
Ahora mismo una tarea se imprime a mano, campo por campo:
// src/main.rs
for task in store.all() {
println!("#{}: {} - {:?}", task.id, task.title, task.status);
}
Los datos son correctos. Pero el formato es una decisión de main, no de la tarea. Intenta
exigir que la tarea hable por sí misma. Reemplaza la línea de salida por:
// src/main.rs
println!("{}", task);
El compilador lo nota:
error[E0277]: `Task` doesn't implement `std::fmt::Display`
--> src/main.rs:XX:XX
|
XX | println!("{}", task);
| ^^^^ `Task` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Task`
= note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
{:?} funciona porque #[derive(Debug)] ya está en Task. {} requiere algo diferente.
Lo que sabe el compilador
{} y {:?} son dos llamadas diferentes a dos traits diferentes.
{:?} llama a fmt::Debug. Lo genera #[derive(Debug)]: muestra los campos y valores tal
como se almacenan en memoria. Por eso exactamente puede derivarse automáticamente - la
estructura ya la conoce el compilador.
{} llama a fmt::Display. No puede derivarse automáticamente: el compilador no sabe qué
significa “legible por humanos” para tu tipo. Esa es la decisión del autor - y solo suya.
El trait es std::fmt::Display. Para evitar escribir la ruta completa cada vez, añade esto
al principio de src/main.rs:
// src/main.rs
use std::fmt;
use y los módulos se cubren en el capítulo 3.6; por ahora basta saber que fmt::Display
y std::fmt::Display son lo mismo.
El trait requiere un método:
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
fmt::Formatter<'_> es el receptor de la cadena formateada; write!(f, ...) escribe en él.
El apóstrofo '_ es una abreviatura para la vida útil de una referencia - eso se cubre en
el capítulo 6. Copia la firma tal cual.
write!(f, ...) funciona como println!, pero escribe en f. El último write! del
método se devuelve directamente - es el fmt::Result.
El conjuro
En src/main.rs, después de impl From<&str> for Task:
// src/main.rs - NUEVO
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}: {} [{:?}]", self.id, self.title, self.status)
}
}
{:?} para status - Status es un enum simple sin datos; su salida Debug (Todo, Done)
se lee igual de bien que cualquier alternativa escrita a mano.
El mismo trait para los errores. En src/main.rs, después de la declaración de TqError:
// src/main.rs - NUEVO
impl fmt::Display for TqError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TqError::EmptyTitle => write!(f, "task title cannot be empty"),
TqError::NotFound(id) => write!(f, "task {} not found", id),
}
}
}
En main, elimina la cadena ensamblada a mano. El bucle se convierte en:
// src/main.rs - CAMBIADO: cadena manual → Display
for task in store.all() {
println!("{}", task);
}
Cada lugar donde los errores se imprimían con {:?} cambia a {}. Por ejemplo:
// src/main.rs - CAMBIADO: {:?} → {}
Err(e) => println!("rejected: {}", e),
Hay una consecuencia más. Antes main hacía match de NotFound en un brazo separado e
imprimía el mensaje a mano:
Err(TqError::NotFound(id)) => println!("task {} not found", id),
Ahora eso duplica Display. El brazo en esa forma es redundante:
// src/main.rs - CAMBIADO: brazo específico eliminado, Display es suficiente
match store.get(99) {
Ok(task) => println!("{}", task),
Err(e) => println!("{}", e),
}
Salida:
#1: Buy coffee [Todo]
#2: Buy milk [Todo]
#3: Buy eggs [Todo]
found: #1: Buy coffee [Todo]
task 99 not found
done: true
rejected: task title cannot be empty
Dos tests fijan ambos formatos:
#[test]
fn task_display_todo() {
let task = Task::new(1, "Buy coffee").unwrap();
assert_eq!(format!("{}", task), "#1: Buy coffee [Todo]");
}
#[test]
fn error_display_not_found() {
assert_eq!(format!("{}", TqError::NotFound(99)), "task 99 not found");
}
make ci pasa. La tarea puede describirse a sí misma - main ya no ensambla cadenas a
partir de campos.
El código completo de
tqpara este capítulo está en3-a-voice/05-a-voice-of-its-own/.
Scroll: Las vidas útiles en Rust - no una duración, sino una dependencia
Lore: std::error::Error
Display es la primera mitad del contrato estándar para tipos de error. La biblioteca
estándar tiene un trait std::error::Error, y requiere Display como condición previa: un
error debe poder describirse en lenguaje llano.
TqError aún no firma este contrato. Pero cuando lleguen los errores de E/S de archivos,
las bibliotecas requerirán std::error::Error para la compatibilidad. Display ya está en
su lugar - esa es una de las dos líneas necesarias.