← Оглавление

2.2 Всё не по плану

Сказать “не нашёл” - одно умение. Сказать “не возьму, и вот почему” - другое. На взгляд мастера разница очевидна; на взгляд большинства инструментов - совершенно нет. Инструмент, умеющий только молча уронить заготовку, когда она оказывается негодной, не намного честнее того, который просто паникует. Разница лишь в том, кто слышит грохот.

Что нужно

Task::new(1, "") создаёт задачу без названия и не возражает. Позже она появится в списке, займёт свое место, и узнать, откуда взялась и что означает, уже не получится. Нужен способ сказать на входе: эта задача не годится - и объяснить почему.

Нужен тип, который несёт либо значение, либо причину отказа - и оставляет решение вызывающему коду.

Сборка

Начнём с ошибки. TqError - перечисление причин отказа. Начнем с одной:

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

Result<T, E> - стандартный тип Rust для операций, которые могут завершиться успехом или неудачей:

Меняем 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 покажет оператор ?, который избавляет от этого выбора в большинстве случаев.