Перейти к содержимому

Result в Rust

Если Option описывает значение, которого может не быть, то 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 значит «недопустимая цифра»: в строке встретился символ, который не является цифрой.

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): именно туда ему нужно вернуть ошибку.

В Rust есть ещё panic!, который сразу останавливает программу, — подробнее о нём на следующей странице. Простое правило: используй Result для неудач, которые вызывающий код вполне может захотеть обработать (неправильный ввод, отсутствующий файл); panic! оставь для ситуаций, которые означают настоящую ошибку в программе, когда продолжать хуже, чем остановиться.

Когда функции начинают ломаться несколькими разными способами, обычной String в качестве ошибки становится мало. На странице Свои типы ошибок показано, как объявить собственный тип ошибки, а на странице anyhow и thiserror — крейты, которыми большинство проектов на Rust сокращают шаблонный код.

Напиши 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 — слишком большой возраст для правды
▶ Попробуй сам

Напиши 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