Result в Rust
Если Option описывает значение, которого может не быть, то Result описывает операцию, которая может не получиться. И в отличие от отсутствующего значения, у неудачи обычно есть причина.
Как выглядит Result
Заголовок раздела «Как выглядит Result»Result — ещё одно перечисление из стандартной библиотеки, на этот раз с двумя вариантами, и оба несут данные:
enum Result<T, E> { Ok(T), // успех, внутри значение Err(E), // неудача, внутри ошибка}Функция, которая может не сработать
Заголовок раздела «Функция, которая может не сработать».parse(), с которым ты познакомился на странице Приведение типов, — хороший настоящий пример. Превратить текст в число не получится, если текст на самом деле не число:
fn main() { let good: Result<i32, std::num::ParseIntError> = "42".parse(); let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good); println!("{:?}", bad);}Вывод:
Ok(42)Err(ParseIntError { kind: InvalidDigit })InvalidDigit значит «недопустимая цифра»: в строке встретился символ, который не является цифрой.
Обрабатываем Result через match
Заголовок раздела «Обрабатываем Result через match»fn main() { let input = "42";
match input.parse::<i32>() { Ok(number) => println!("Получилось число: {number}"), Err(e) => println!("Не удалось разобрать: {e}"), }}Форма та же, что и при обработке Option через match. Только ветка Err даёт тебе настоящее значение ошибки, которое можно изучить или показать, а не просто «здесь ничего нет».
Пишем свою функцию, которая возвращает Result
Заголовок раздела «Пишем свою функцию, которая возвращает Result»fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err(String::from("на ноль делить нельзя")) } else { Ok(a / b) }}
fn main() { match divide(10.0, 2.0) { Ok(result) => println!("Результат: {result}"), Err(e) => println!("Ошибка: {e}"), }
match divide(10.0, 0.0) { Ok(result) => println!("Результат: {result}"), Err(e) => println!("Ошибка: {e}"), }}Вывод:
Результат: 5Ошибка: на ноль делить нельзяТип Result<f64, String> заставляет каждого, кто вызывает функцию, разобраться с возможной неудачей. Случайно принять деление на ноль за обычное число не получится.
Оператор ?
Заголовок раздела «Оператор ?»Цепочка из нескольких операций, каждая из которых может не сработать, приводит к глубоко вложенным блокам match. Оператор ? делает код плоским. Он значит: «если здесь Err, сразу верни эту ошибку из текущей функции; иначе дай мне значение из Ok».
fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> { let number = input.parse::<i32>()?; // при ошибке разбора сразу возвращаемся Ok(number * 2)}
fn main() { println!("{:?}", parse_and_double("21")); println!("{:?}", parse_and_double("ой"));}Вывод:
Ok(42)Err(ParseIntError { kind: InvalidDigit })Без ? этой строке понадобился бы свой match, только чтобы достать number или досрочно выйти с ошибкой. ? делает ровно это одним символом. Он работает только внутри функции, которая сама возвращает Result (или Option): именно туда ему нужно вернуть ошибку.
Result или panic!
Заголовок раздела «Result или panic!»В Rust есть ещё panic!, который сразу останавливает программу, — подробнее о нём на следующей странице. Простое правило: используй Result для неудач, которые вызывающий код вполне может захотеть обработать (неправильный ввод, отсутствующий файл); panic! оставь для ситуаций, которые означают настоящую ошибку в программе, когда продолжать хуже, чем остановиться.
Когда функции начинают ломаться несколькими разными способами, обычной String в качестве ошибки становится мало. На странице Свои типы ошибок показано, как объявить собственный тип ошибки, а на странице anyhow и thiserror — крейты, которыми большинство проектов на Rust сокращают шаблонный код.
Практика
Заголовок раздела «Практика»1. Проверяем возраст
Заголовок раздела «1. Проверяем возраст»Напиши fn parse_age(input: &str) -> Result<u32, String>. Она должна возвращать Err с понятным сообщением, если текст — не число или если число больше 150. Проверь её на "34", "abc" и "200". Подсказка: сделай match по input.parse::<u32>(), а внутри ветки Ok проверь величину через if.
Показать решение
fn parse_age(input: &str) -> Result<u32, String> { match input.parse::<u32>() { Ok(age) => { if age > 150 { Err(format!("{age} — слишком большой возраст для правды")) } else { Ok(age) } } Err(_) => Err(format!("'{input}' — не число")), }}
fn main() { for input in ["34", "abc", "200"] { match parse_age(input) { Ok(age) => println!("Возраст: {age}"), Err(e) => println!("Ошибка: {e}"), } }}Вывод:
Возраст: 34Ошибка: 'abc' — не числоОшибка: 200 — слишком большой возраст для правды2. Складываем два числа через ?
Заголовок раздела «2. Складываем два числа через ?»Напиши fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>, которая разбирает обе строки и возвращает их сумму. Используй ? вместо match. Выведи результат для ("10", "20") и ("10", "двадцать"). Подсказка: нужен один ? на каждый parse, а последняя строка заворачивает сумму в Ok(...).
Показать решение
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> { let x = a.parse::<i32>()?; let y = b.parse::<i32>()?; Ok(x + y)}
fn main() { println!("{:?}", add_strings("10", "20")); println!("{:?}", add_strings("10", "двадцать"));}Вывод:
Ok(30)Err(ParseIntError { kind: InvalidDigit })Result<T, E>описывает операцию, которая может завершиться успехом (Ok) или неудачей (Err) со значением ошибки.matchявно обрабатывает оба случая — так же, как сOption.- Оператор
?сам досрочно возвращаетErr, так что не нуженmatchпосле каждого вызова, который может не сработать. - Для ошибок, после которых вызывающий код может разумно восстановиться, предпочитай
Result, а неpanic!.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Какие два варианта есть у Result<T, E>?
2. Что делает оператор ? внутри функции, которая возвращает Result?
3. Когда, согласно этой странице, стоит предпочесть Result, а не panic!?
4. Что возвращает divide(10.0, 0.0) в примере с делением на этой странице?
5. Почему оператор ? работает только внутри функции, которая сама возвращает Result (или Option)?
Результат: 0 / 5
Что почитать
Заголовок раздела «Что почитать»- Устранимые ошибки с Result — книга «Язык программирования Rust»: официальная глава о
Result,unwrap,expectи операторе?, с примерами открытия файлов (перевод на русский). std::result— документация стандартной библиотеки (на английском): обзор работы сResult, включая методы вродеmap_err,unwrap_orиok.- Result — Rust на примерах: запускаемые примеры обработки
Resultи возврата его изmain(перевод на русский). - Оператор ? — Rust на примерах: короткий и точный разбор того, как
?заменяет вложенные блокиmatch(перевод на русский).