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:
-
.unwrap()- “Я уверен, что здесьOk. Если нет - пусть программа упадёт”. Подходит в тестах и в коде с заведомо корректными данными. -
?- “Если ошибка - передаю её выше”. Только в функциях, возвращающихResult. Убирает шум, сохраняет смысл. -
match- “Обрабатываю оба случая здесь”. Нужен, когда ошибка требует специфического поведения: залогировать, подставить значение по умолчанию, попробовать снова.
В main цепочка заканчивается - ошибке некуда идти дальше. Поэтому здесь нередко используют
? для промежуточных вызовов и просто позволяют main вернуть Err с выводом через Debug.