2.2 Всё не по плану
Сказать “не нашёл” - одно умение. Сказать “не возьму, и вот почему” - другое. На взгляд мастера разница очевидна; на взгляд большинства инструментов - совершенно нет. Инструмент, умеющий только молча уронить заготовку, когда она оказывается негодной, не намного честнее того, который просто паникует. Разница лишь в том, кто слышит грохот.
Что нужно
Task::new(1, "") создаёт задачу без названия и не возражает. Позже она появится в
списке, займёт свое место, и узнать, откуда взялась и что означает, уже не получится.
Нужен способ сказать на входе: эта задача не годится - и объяснить почему.
Нужен тип, который несёт либо значение, либо причину отказа - и оставляет решение вызывающему коду.
Сборка
Начнём с ошибки. TqError - перечисление причин отказа. Начнем с одной:
#[derive(Debug)]
enum TqError {
EmptyTitle,
}
Result<T, E> - стандартный тип Rust для операций, которые могут завершиться успехом
или неудачей:
Ok(value)- операция успешна, внутри результатErr(reason)- операция не удалась, внутри причина
Меняем Task::new. Было: -> Task. Стало: -> Result<Task, TqError>:
fn new(id: u64, title: &str) -> Result<Task, TqError> {
if title.is_empty() {
return Err(TqError::EmptyTitle);
}
Ok(Task {
id,
title: String::from(title),
status: Status::Todo,
})
}
Если заголовок пустой - возвращаем Err(TqError::EmptyTitle). Иначе - оборачиваем
задачу в Ok(...). Оба варианта имеют один тип: Result<Task, TqError>.
Теперь любой, кто вызывает Task::new, получает не задачу, а Result - и обязан
разобрать оба случая. Молча проигнорировать ошибку больше не получится.
Там, где задача заведомо корректна, можно временно использовать .unwrap():
add(&mut tasks, Task::new(1, "Buy coffee").unwrap());
Там, где ошибка возможна - обрабатываем явно:
match Task::new(4, "") {
Ok(_) => println!("task created"),
Err(e) => println!("error: {:?}", e),
}
Два варианта - два теста:
#[test]
fn new_task_rejects_empty_title() {
assert!(Task::new(1, "").is_err());
}
#[test]
fn new_task_accepts_valid_title() {
assert!(Task::new(1, "Buy coffee").is_ok());
}
Полный набор тестов - в tq/.
Итог
main ниже показывает только новое - .unwrap() на корректных задачах и явную обработку
ошибки. Полный накопленный код - в tq/.
fn main() {
let mut tasks: Vec<Task> = Vec::new();
add(&mut tasks, Task::new(1, "Buy coffee").unwrap());
add(&mut tasks, Task::new(2, "Buy milk").unwrap());
add(&mut tasks, Task::new(3, "Buy eggs").unwrap());
match Task::new(4, "") {
Ok(_) => println!("task created"),
Err(e) => println!("error: {:?}", e),
}
}
error: EmptyTitle
make ci проходит. Task::new теперь отказывается принимать пустое название - и объясняет
почему.
Полный код
tqдля этой главы -2-honest/02-when-things-go-wrong/.
Lore: .unwrap() и когда его использовать
.unwrap() - явный выбор: “Я уверен, что здесь Ok, и если нет - пусть программа
упадёт”. В коде выше это оправдано: заголовки задач известны заранее и точно не пустые.
В коде, который принимает данные снаружи, .unwrap() - уже риск. Глава 2.3 покажет
оператор ?, который избавляет от этого выбора в большинстве случаев.