← Оглавление

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/.

Свиток: Rust lifetimes - not a duration, but a dependency


Lore: std::error::Error

Display - первая половина стандартного контракта для типов ошибок. В стандартной библиотеке есть трейт std::error::Error, и он требует Display как обязательное условие: ошибка обязана уметь описать себя понятным языком.

TqError пока этот контракт не подписывает. Но когда появятся ошибки файлового ввода-вывода, библиотеки потребуют std::error::Error для совместимости. Display уже есть - это одна из двух нужных строк.