3.5 Свой голос
Подписать обязательство - не последний шаг. Мастер, умеющий делать изделия, но не умеющий их описывать, работает для одной аудитории. Технический отчёт точен и полон - для того, кто его составлял. Это принято называть точностью. Правильное слово - половиной работы.
Попытка
Сейчас задача выводится по полям вручную:
// src/main.rs
for task in store.all() {
println!("#{}: {} - {:?}", task.id, task.title, task.status);
}
Данные верные. Но формат - это решение main, а не задачи. Попробуем потребовать от задачи
говорить за себя. Замените строку вывода на:
// src/main.rs
println!("{}", task);
Компилятор замечает:
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
{:?} работает потому, что #[derive(Debug)] уже есть на Task. {} требует другого.
Что замечает компилятор
{} и {:?} - два разных вызова к двум разным трейтам.
{:?} вызывает fmt::Debug. Его генерирует #[derive(Debug)]: показывает поля и значения
в том виде, в котором они хранятся в памяти. Именно поэтому его можно вывести автоматически -
структура уже известна компилятору.
{} вызывает fmt::Display. Его нельзя вывести автоматически: компилятор не знает, что
“человекочитаемый” значит для вашего типа. Это решение автора - и только его.
Трейт называется std::fmt::Display. Чтобы не писать полный путь каждый раз, добавьте в
начало src/main.rs:
// src/main.rs
use std::fmt;
use и модули разбираются в M3.6; пока достаточно знать: fmt::Display и
std::fmt::Display - одно и то же.
Трейт требует одного метода:
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
fmt::Formatter<'_> - получатель форматированной строки; write!(f, ...) пишет в него.
Апостроф '_ - сокращение для времени жизни ссылки, это разбирается в M6; сигнатуру
копируйте как есть.
write!(f, ...) работает как println!, только пишет в f. Последний write! в методе
возвращается напрямую - он и есть fmt::Result.
Заклинание
В src/main.rs, после impl From<&str> for Task:
// src/main.rs - NEW
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}: {} [{:?}]", self.id, self.title, self.status)
}
}
{:?} для status - Status простой перечислимый тип без данных, его Debug-вывод
(Todo, Done) читается не хуже любой ручной альтернативы.
Тот же трейт для ошибок. В src/main.rs, после объявления TqError:
// src/main.rs - NEW
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),
}
}
}
В main убираем ручную сборку. Цикл становится:
// src/main.rs - CHANGED: ручная строка → Display
for task in store.all() {
println!("{}", task);
}
Все места, где ошибки выводились через {:?}, переходят на {}. Например:
// src/main.rs - CHANGED: {:?} → {}
Err(e) => println!("rejected: {}", e),
Есть ещё одно следствие. Ранее main разбирал NotFound отдельной веткой и печатал
сообщение вручную:
Err(TqError::NotFound(id)) => println!("task {} not found", id),
Теперь это дублирует Display. Ветка в такой форме избыточна:
// src/main.rs - CHANGED: специфическая ветка убрана, Display достаточно
match store.get(99) {
Ok(task) => println!("{}", task),
Err(e) => println!("{}", e),
}
Вывод:
#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
Два теста фиксируют оба формата:
#[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 проходит. Задача умеет описывать себя сама - main больше не собирает строку по
полям.
Полный код
tqдля этой главы -3-a-voice/05-a-voice-of-its-own/.
Lore: std::error::Error
Display - первая половина стандартного контракта для типов ошибок. В стандартной
библиотеке есть трейт std::error::Error, и он требует Display как обязательное
условие: ошибка обязана уметь описать себя понятным языком.
TqError пока этот контракт не подписывает. Но когда появятся ошибки файлового
ввода-вывода, библиотеки потребуют std::error::Error для совместимости. Display уже
есть - это одна из двух нужных строк.