← Оглавление

2.4 Имя для пустоты

Посыльный, вернувшийся с пустыми руками, полезен ровно настолько, насколько умеет объяснить, чего именно не нашёл. “Не нашёл” - это конец разговора. “Не нашёл вот это” - начало следующего. Большинство инструментов предпочитают первое. Это принято называть лаконичностью, хотя правильное слово несколько другое.

Что нужно

get возвращает Option<&Task>. В M2.1 это было верным решением: задача либо есть, либо нет, причин у отсутствия нет - только факт. С тех пор tq изменился. Теперь у ошибок есть типы, у функций - общий язык: Result.

У Option два ограничения в нынешнем контексте.

Первое - ? с ним не работает. main возвращает Result<(), TqError>, и попытка применить ? к Option<&Task> вызовет ошибку компилятора: Option и Result - разные типы.

Второе - отсутствие анонимно. None говорит “нет”. Не говорит “нет задачи с id 42”. Когда ошибка уходит вверх, вызывающий теряет контекст: какая именно задача не была найдена?

TqError умеет нести информацию. Добавим вариант NotFound(u64) - он будет содержать id задачи. get должен вернуть именно его.

Сборка

Сначала добавим новый вариант ошибки:

#[derive(Debug)]
enum TqError {
    EmptyTitle,
    NotFound(u64),
}

При отладке это выглядит как NotFound(42) - конкретно и без домыслов.

Теперь get. Старая версия возвращала Option<&Task> через find. Новая использует .ok_or(...) - метод Option, который превращает None в Err(e), а Some(v) в Ok(v):

fn get(tasks: &[Task], id: u64) -> Result<&Task, TqError> {
    tasks.iter().find(|task| task.id == id).ok_or(TqError::NotFound(id))
}

find ищет, .ok_or(...) оборачивает отсутствие в именованную ошибку с нужным id.

В main два вызова get - оба используют match. Паттерны меняются с Some/None на Ok/Err:

match get(&tasks, 1) {
    Ok(task) => println!("found: #{}: {}", task.id, task.title),
    Err(e) => println!("error: {:?}", e),
}

match get(&tasks, 99) {
    Ok(task) => println!("#{}: {}", task.id, task.title),
    Err(TqError::NotFound(id)) => println!("task {} not found", id),
    Err(e) => println!("error: {:?}", e),
}

Второй match деструктурирует NotFound(id) - извлекает id из ошибки и использует его напрямую.

Ветка Err(e) в этом match сейчас недостижима: get производит только NotFound, других ошибок у него нет. Но компилятор видит TqError целиком и требует покрыть все варианты. Такой catch-all - принятый в Rust приём: когда TqError вырастет и появятся новые варианты, эта ветка поймает их без изменений в коде.

Итог

cargo run теперь выводит:

task 99 not found

Не “not found” - task 99 not found. Посыльный назвал, чего не нашёл.

? теперь работает с get. Функция, которая ищет задачу и хочет передать ошибку выше, может написать get(&tasks, id)? - и NotFound(id) уйдёт к вызывающему без лишнего match.

Один новый тест для наших правок: проверяет, что id сохраняется в ошибке:

#[test]
fn get_carries_missing_id_in_error() {
    let tasks: Vec<Task> = Vec::new();
    assert!(matches!(get(&tasks, 99), Err(TqError::NotFound(99))));
}

matches!(expr, pattern) возвращает true, если выражение совпадает с паттерном - включая вложенные значения. Полный набор тестов - в tq/.

make ci проходит. get больше не молчит о том, что именно искал.

Полный код tq для этой главы - 2-honest/04-a-name-for-the-absent/.


Lore: Option и Result - два способа сказать “нет”

Option говорит: значение либо есть, либо нет. Без деталй - только факт. Используй его, когда отсутствие - нормальный исход, не требующий объяснения. Vec::first() возвращает Option<&T>: пустой вектор - не ошибка, просто состояние.

Result говорит: операция либо удалась, либо нет - и если нет, вот почему. Используй его, когда вызывающий должен знать причину.

Практическое правило: если None - нормальный рабочий исход, используй Option. Если None означает “что-то пошло не так” - используй Result.