← Оглавление

2.3 Чужая проблема

Молоток не решает, годится ли дерево. Его дело - гвоздь; всё остальное - забота плотника, который держит рукоять. Инструмент, принимающий решение за держащего, работает тихо - и при этом присваивает чужой контекст. Чужой контекст имеет привычку оказываться важным именно тогда, когда его уже не спросили.

Попытка

В прошлой главе .unwrap() появился во всех вызовах Task::new в main: “заголовки задач известны заранее, ошибок не будет”. Пока данные приходят из кода - это честно. Как только они начнут поступать снаружи, “известны заранее” перестанет быть правдой.

Первый шаг - убрать .unwrap() и передавать ошибку выше. В Rust для этого есть оператор ?. Попробуем заменить один вызов:

fn main() {
    let mut tasks: Vec<Task> = Vec::new();
    add(&mut tasks, Task::new(1, "Buy coffee")?);

Компилятор замечает:

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`
              (or another type that implements `FromResidual`)
  --> src/main.rs:XX:XX
   |
XX | fn main() {
   | --------- this function should return `Result` or `Option` to accept `?`
...
XX |     add(&mut tasks, Task::new(1, "Buy coffee")?);
   |                                               ^ cannot use the `?` operator in a function that returns `()`
   |
   = help: the trait `FromResidual<Result<Infallible, TqError>>` is not implemented for `()`

main возвращает (). Оператор ? требует другого.

Что знает компилятор

? - сокращение для обработки Result. Код:

Task::new(1, "Buy coffee")?

разворачивается примерно в:

match Task::new(1, "Buy coffee") {
    Ok(task) => task,
    Err(e) => return Err(e),
}

Если Ok(task) - продолжаем, значение доступно дальше. Если Err(e) - немедленно возвращаем ошибку вызывающему. Именно поэтому ? требует, чтобы текущая функция возвращала Result: ей нужно куда отправить ошибку.

В Rust main может возвращать Result<(), E>, если E поддерживает Debug. Изменим сигнатуру:

fn main() -> Result<(), TqError> {

Теперь у ? есть куда передавать ошибки. Если main завершится с Err, Rust выведет значение через Debug и выйдет с ненулевым кодом.

Решение

Меняем .unwrap() на ? во всех вызовах Task::new, добавляем явный успешный результат в конце:

fn main() -> Result<(), TqError> {
    let mut tasks: Vec<Task> = Vec::new();

    add(&mut tasks, Task::new(1, "Buy coffee")?);
    list(&tasks);

    add(&mut tasks, Task::new(2, "Buy milk")?);
    add(&mut tasks, Task::new(3, "Buy eggs")?);

    let task = &mut tasks[0];
    task.complete();

    if task.is_done() {
        println!("Task 1 done");
    }

    list(&tasks[1..]);

    match get(&tasks, 1) {
        Some(task) => println!("#{}: {}", task.id, task.title),
        None => println!("not found"),
    }

    match get(&tasks, 99) {
        Some(task) => println!("#{}: {}", task.id, task.title),
        None => println!("not found"),
    }

    Ok(())
}

Ok(()) в конце - явный успешный результат. Без него функция с возвращаемым типом Result<(), TqError> не скомпилировалась бы: тело должно вернуть Result, а не ().

Логика Task::new не изменилась - тесты из предыдущей главы в tq/ по-прежнему проходят.

make ci проходит. Ошибки из Task::new больше не решаются на месте - они уходят вверх, к тому, кто знает, что с ними делать.

Полный код tq для этой главы - 2-honest/03-passing-the-problem-upward/.


Lore: .unwrap(), ? и явный match

Три способа работать с Result:

В main цепочка заканчивается - ошибке некуда идти дальше. Поэтому здесь нередко используют ? для промежуточных вызовов и просто позволяют main вернуть Err с выводом через Debug.