Pull to refresh

Идиоматическая обработка ошибок в Rust

Reading time7 min
Views4.7K
Original author: Nicholas Rempel

Вы забудете о необработанных исключениях, если научитесь обращаться с ошибками, как это принято в Rust


Обработка ошибок – важнейшая часть любого языка программирования, а в Rust предлагается для этого уникальный мощный подход. Rust, в отличие от многих других языков программирования, не предлагает исключений, а предоставляет перечисление Result. У разработчика нет иного выхода, кроме как обрабатывать все ошибки согласованно и предсказуемо. Поэтому ошибки становится проще идентифицировать и диагностировать.

Поскольку в Rust не предусмотрены исключения, любая функция обязана возвращать либо значение, либо "панику". Когда функция паникует, процесс сразу же завершается и предоставляет вызывающей стороне конкретную обратную связь. Чисто технически можно отлавливать случаи паники в Rust при помощи catch_unwind, но постоянно так делать не рекомендуется. Вместо этого в Rust предлагается перечисление Result; пользуясь им, разработчик вынужден обрабатывать все ошибки сам.

В этом посте будут рассмотрены применяемые в Rust идиоматические паттерны обработки ошибок, и вы в общих чертах поймёте, как это делается. Мы разберём перечисление Result, посмотрим, как с его помощью обрабатываются ошибки в программах Rust, и какие крейты чаще всего используются, чтобы упростить этот процесс.

image

Тип Result


Если функция подвержена ошибкам — то есть, она может тем или образом отказать — то обычно она возвращает тип Result, предусмотренный в Rust. При возврате Result от функции разработчик обязан возвращать её в одном из существующих вариантов: Result::Ok и Result::Err. Так как эти варианты так распространены, они предоставляются в prelude, поэтому можно написать просто Ok или Err.

Поскольку Result – это перечисление, не составляет труда сопоставить его с ошибкой и обработать любой из случаев:

fn fallible(succeed: bool) -> Result<&'static str, &'static str> {
    if succeed {
        return Ok("success!");
    }
    Err("this is an error message")
}

fn main() -> Result<(), &'static str> {
    let result = fallible(false);
    let value = match result {
        Ok(value) => value,
        Err(err) => {
            return Err(err);
        }
    };

    println!("got a value: {value}");
    Ok(())
}

Как видите, здесь можно воспользоваться стандартным оператором match, предусмотренным в Rust. Этот оператор предусмотрен для ветвления в любой из вариантов перечисления. В Rust функция не выбрасывает исключения, а либо выполняет panic (в идеале, такого не должно происходить ни в коем случае), либо возвращает тип Result. В этом искусственном примере код всегда возвращает ошибку, но можно представить себе и более сложную функцию, которая отказывала бы каким-нибудь неожиданным образом.

Rust разрешает возвращать Result даже в функции main. Если значение, возвращённое из функции main, является ошибкой, то Rust выведет эту ошибку в отладочном представлении Debug и завершит процесс, сопроводив его кодом ошибки.

Оператор «вопросительный знак»


Когда в базе кода активно используется Result, обрабатывать каждый отдельный случай ошибки может быть затруднительно. Чтобы упростить эту ситуацию, в Rust предлагается оператор «вопросительный знак», при помощи которого мы сокращённо представляем развёртку успешного результата или возвращаем ошибку вызывающей стороне. В сущности, этот оператор «вопросительный знак» доставляет ошибку вызывающей стороне, обеспечивая «всплытие» этой ошибки.

Например, вот как можно значительно упростить предыдущий пример:

fn fallible(succeed: bool) -> Result<&'static str, &'static str> {
    if succeed {
        return Ok("success!");
    }
    Err("this is an error message")
}

fn main() -> Result<(), &'static str> {
    let value = fallible(false)?;
    println!("got a value: {value}");
    Ok(())
}

Этот пример полностью эквивалентен первому, но здесь при помощи оператора? мы с лёгкостью извлекаем интересующее нас значение из Result::Ok, если оно там есть. В противном случае возвращаем вызывающей стороне вариант Result::Error.

Ошибки при упаковке


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

Рассмотрим следующий пример:

use reqwest::blocking::get;

fn download() -> Result<String, reqwest::Error> {
    let website_text = get("https://www.rust-lang.org")?.text()?;
    Ok(website_text)
}

fn main() -> Result<(), reqwest::Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

В этом примере мы скачиваем информацию с сайта https://www.rust-lang.org и используем оператор ?, чтобы упростить обработку ошибок. Обратите внимание: второй тип, передаваемый каждому из Result – это теперь reqwest::Error. Этот тип нам подходит, поскольку и get(), и text() возвращают Result с ошибкой типа reqwest::Error.

Теперь допустим, что вы также хотите сохранить текст со скачанного сайта в отдельном файле. Легко дополнить наш код, чтобы в нём выполнялась и такая операция:

use tempfile::tempfile;
use std::io::copy;
use reqwest::blocking::get;

fn download() -> Result<String, reqwest::Error> {
    let mut file = tempfile()?;
    let website_text = get("https://www.rust-lang.org")?.text()?;
    copy(&mut website_text.as_bytes(), &mut file)?;
    Ok(website_text)
}

fn main() -> Result<(), reqwest::Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

Но этот код не компилируется! При попытке его собрать получаем следующую ошибку:

  |
5 | fn download() -> Result<String, reqwest::Error> {
  |                  ------------------------------ expected `reqwest::Error` because of this
6 |     let mut file = tempfile()?;
  |                              ^ the trait `From<std::io::Error>` is not implemented for `reqwest::Error`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = help: the following other types implement trait `FromResidual<R>`:
            <Result<T, F> as FromResidual<Result<Infallible, E>>>
            <Result<T, F> as FromResidual<Yeet<E>>>
  = note: required for `Result<String, reqwest::Error>` to implement `FromResidual<Result<Infallible, std::io::Error>>`

Такая ошибка возникает, так как tempfile() возвращает тип ошибки, не совпадающий с сигнатурой нашей функции Result<String, reqwest::Error>. Проще всего решить эту проблему так: «упаковать» ошибку, а затем вернуть вместо неё Result<String, Box<dyn std::error::Error>>.

В стандартной библиотеке предоставляется такой типаж std::error::Error, предназначенный для обработки как раз тех случаев, в которых нам требуется возвращать по несколько типов ошибок. Ошибки тех типов, что предоставляются в библиотеках, должны реализовывать Error, чтобы мы могли без труда преобразовывать различные типы ошибок в обобщённый типаж object. Опять же, оператор «вопросительный знак» это сильно упрощает до степени «просто работает».

use reqwest::blocking::get;
use std::error::Error;
use std::io::copy;
use tempfile::tempfile;

fn download() -> Result<String, Box<dyn Error>> {
    let mut file = tempfile()?;
    let website_text = get("https://www.rust-lang.org")?.text()?;
    copy(&mut website_text.as_bytes(), &mut file)?;
    Ok(website_text)
}

fn main() -> Result<(), Box<dyn Error>> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

Также стоит отметить anyhow, ещё один популярный крейт, который схожим образом позволяет использовать типаж Box>, но с некоторыми дополнениями, которые повышают эргономичность работы.

Использование thiserror


Есть такой недостаток при использовании anyhow или Box>: мы теряем информацию о типах ошибок, возвращаемых функции, а знать типы зачастую полезно, когда программируешь логику ветвления в зависимости от того, какая именно ошибка возникла.

При отслеживании контекста ошибки действует такое соглашение: собираем наше собственное перечисление ошибок, в котором описываются все типы ошибок, что могут произойти в данном крейте или модуле.

use reqwest::blocking::get;
use std::io::copy;
use tempfile::tempfile;

fn download() -> Result<String, Error> {
    let mut file = tempfile().map_err(|_| Error::File)?;
    let website_text = get("https://www.rust-lang.org")
        .map_err(|_| Error::Download)?
        .text()
        .map_err(|_| Error::Download)?;
    copy(&mut website_text.as_bytes(), &mut file).map_err(|_| Error::File)?;
    Ok(website_text)
}

fn main() -> Result<(), Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

#[derive(Debug)]
enum Error {
    File,
    Download,
}

Теперь вызывающая сторона знает контекст и тип той ошибки, которая возникла в функции скачивания. Но нам требуется вручную преобразовать ошибки, чтобы поместить их в наше пользовательское перечисление Error. Для этого применяется функция map_err, довольно неудобная и многословная.

При помощи Thiserror проще отображать типы ошибок на созданные вами пользовательские типы ошибок:

use reqwest::blocking::get;
use std::io::copy;
use tempfile::tempfile;
use thiserror::Error;

fn download() -> Result<String, Error> {
    let mut file = tempfile()?;
    let website_text = get("https://www.rust-lang.org")?.text()?;
    copy(&mut website_text.as_bytes(), &mut file)?;
    Ok(website_text)
}

fn main() -> Result<(), Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

#[derive(Debug, Error)]
enum Error {
    #[error("file error: {0}")]
    File(#[from] std::io::Error),
    #[error("download error: {0}")]
    Download(#[from] reqwest::Error),
}

Обратите внимание, как нам удалось сохранить отображение ошибок на наше собственное декларативное перечисление. Для этого мы воспользовались макросами, которые предоставляются в thiserror. Наш код остаётся чистым, а для контролируемого просачивания ошибок в нём мы можем с лёгкостью воспользоваться оператором «вопросительный знак».

Заключение


Обработка ошибок – неотъемлемая составляющая любого языка программирования, а в Rust она реализована особенно хорошо и надёжно. Rust, в отличие от многих других языков программирования, вообще не использует исключений, а перечисляет вместо них перечисление Result, при работе с которым программисту приходится самостоятельно обрабатывать все ошибки. Программировать в таком стиле не только эффективнее, но и приятнее. Благодаря перечислению Result очень просто осмысливать возникающие условия ошибок и справляться с ними в коде.

____
P.S.
Также обращаем ваше внимание на то, что у нас на сайте проходит распродажа.
Tags:
Hubs:
Total votes 19: ↑16 and ↓3+17
Comments6

Articles

Information

Website
piter.com
Registered
Founded
Employees
201–500 employees
Location
Россия