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.