Обработка восстанавливаемых ошибок с помощью Result

RustBeginner
Практиковаться сейчас

Введение

Некоторые сбои являются ошибками программирования, но другие ожидаемы во время выполнения: пользователь может ввести текст в неправильном формате или значение за пределами допустимого диапазона. Rust представляет такие восстанавливаемые результаты с помощью Result<T, E>, который содержит либо Ok(T), либо Err(E).

Вы разберёте корректный и некорректный текст без аварийного завершения программы, вернёте полезные ошибки проверки и используете оператор ?, чтобы передавать ошибки вызывающему коду. Подготовленный проект Cargo позволяет сосредоточиться на обработке ошибок.

Сопоставление успешного результата Result

На этом этапе вы преобразуете текст в целое число и сопоставите успешный результат.

Проект находится в /home/labex/project/result-workshop, а файл src/main.rs содержит четыре специально подготовленных места для вставки кода. Перейдите в каталог проекта и откройте исходный файл:

cd /home/labex/project/result-workshop
nano src/main.rs

Замените комментарий Step 1 следующим кодом:

    let valid = "42".parse::<i32>();
    match valid {
        Ok(value) => println!("Parsed value: {value}"),
        Err(error) => println!("Parse error: {error}"),
    }

parse — универсальный метод, который может возвращать значения разных типов. Часть ::<i32> задаёт тип результата для этого вызова и просит Rust интерпретировать строку как знаковое 32-битное целое число. Этот аргумент типа в угловых скобках отличается от пути вроде Message::Text, хотя в обоих случаях используется ::. Преобразование может завершиться ошибкой, поэтому возвращаемый тип — Result. В Ok(value) содержится преобразованное целое число, а Err(error) содержит информацию о причине сбоя. Конструкция match должна обработать оба варианта.

Сохраните файл с помощью Ctrl+O, нажмите Enter, а затем выйдите с помощью Ctrl+X. Проверьте и запустите программу:

cargo check
cargo run --quiet
Parsed value: 42

Выполнилась только ветвь Ok, потому что "42" — корректное представление целого числа.

Обработка некорректного ввода

На этом этапе вы обработаете текст в неправильном формате как данные, не позволяя ему аварийно завершить программу.

Паника означает, что программа достигла состояния, которое не было предусмотрено её разработчиком, и обычно прекращает выполнение. Некорректные внешние данные ожидаемы, поэтому они должны оставаться значением Err, которое программа может проверить и вывести. Откройте исходный файл:

nano src/main.rs

Замените комментарий Step 2 следующим кодом:

    let invalid = "many".parse::<i32>();
    match invalid {
        Ok(value) => println!("Unexpected value: {value}"),
        Err(error) => println!("Handled invalid input: {error}"),
    }

Эта конструкция match по-прежнему охватывает оба возможных результата, но подготовленный текст выбирает ветвь Err. Сохраните файл и выйдите из редактора, затем запустите программу:

cargo run --quiet

Вторая строка должна выглядеть так:

Handled invalid input: invalid digit found in string

Программа продолжила работу в обычном режиме и преобразовала ошибку анализатора в понятное сообщение. Не используйте unwrap() для внешних данных: при таком же значении Err функция unwrap() вызвала бы панику вместо возможности обработать ошибку.

Разбор и проверка с помощью оператора вопросительного знака

На этом этапе вы создадите функцию, которая может вернуть либо процентное значение, либо понятное сообщение об ошибке.

Тип Result<u32, String> означает, что при успехе возвращается беззнаковое целое число, а при ошибке — строка с принадлежащим функции текстом ошибки. Оператор ? извлекает значение из Ok; если он встречает Err, то немедленно возвращает эту ошибку из текущей функции.

Откройте исходный файл:

nano src/main.rs

Добавьте эту функцию перед main:

fn parse_percentage(text: &str) -> Result<u32, String> {
    let value = text
        .parse::<u32>()
        .map_err(|_| format!("not a whole number: {text}"))?;

    if value <= 100 {
        Ok(value)
    } else {
        Err(format!("outside 0..=100: {value}"))
    }
}

Рассмотрите функцию как последовательность коротких действий. Сначала parse::<u32>() пытается преобразовать текст в целое число. Затем map_err(...) изменяет только ошибку преобразования, создавая более понятную строку String, содержащую исходный ввод.

Далее оператор ? либо извлекает успешно преобразованное число для переменной value, либо немедленно возвращает новую ошибку из parse_percentage. Если преобразование прошло успешно, конструкция if проверяет число и возвращает либо Ok(value), либо Err для значения за пределами диапазона.

Параметр замыкания _ означает, что исходное значение ошибки анализатора намеренно игнорируется, поскольку его заменяет новое сообщение. Здесь _ — это игнорируемый параметр; выше _ => обозначал шаблонную ветвь сопоставления, а let _ = отбрасывал результат целиком.

Замените комментарий Step 3 внутри main следующим кодом:

    println!("85 => {:?}", parse_percentage("85"));
    println!("150 => {:?}", parse_percentage("150"));
    println!("many => {:?}", parse_percentage("many"));

Форматировщик :? показывает вариант Ok или Err в этом учебном эксперименте. Сохраните файл и выйдите из редактора, затем проверьте и запустите программу:

cargo check
cargo run --quiet

Новые строки должны выглядеть так:

85 => Ok(85)
150 => Err("outside 0..=100: 150")
many => Err("not a whole number: many")

Теперь функция различает корректное значение, успешно разобранное значение за пределами допустимого диапазона и текст в неправильном формате.

Передача ошибки через другую функцию

На этом этапе вы используете ? на границе второй функции и обработаете итоговый результат в main.

Вспомогательная функция не всегда знает, как приложение должно показать ошибку или восстановиться после неё. Она может передать Err выше, оставив решение вызывающему коду. Откройте исходный файл:

nano src/main.rs

Добавьте эту функцию между parse_percentage и main:

fn acceptance_message(text: &str) -> Result<String, String> {
    let value = parse_percentage(text)?;
    Ok(format!("accepted {value}%"))
}

Если преобразование или проверка завершаются ошибкой, оператор ? немедленно возвращает существующую ошибку String. При успехе функция оборачивает своё сообщение в Ok.

Замените комментарий Step 4 внутри main следующим кодом:

    for text in ["73", "bad"] {
        match acceptance_message(text) {
            Ok(message) => println!("{text} => {message}"),
            Err(message) => println!("{text} => error: {message}"),
        }
    }

Цикл использует один корректный и один некорректный ввод. main является границей, на которой каждый результат преобразуется в вывод для пользователя. Сохраните файл и выйдите из редактора, затем проверьте и запустите программу:

cargo check
cargo run --quiet

Последние строки должны выглядеть так:

73 => accepted 73%
bad => error: not a whole number: bad

Тот же контекст ошибки, созданный в parse_percentage, сохранился при передаче через acceptance_message.

Итоги

Вы сопоставили успешные и неуспешные значения Result, обработали некорректный ввод как восстанавливаемую ситуацию, создали контекстные ошибки String с помощью map_err и передали ошибки через границы функций с помощью ?, не вызывая панику через unwrap.