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

от автора

Вы забудете о необработанных исключениях, если научитесь обращаться с ошибками, как это принято в 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.
Также обращаем ваше внимание на то, что у нас на сайте проходит распродажа.


ссылка на оригинал статьи https://habr.com/ru/articles/764416/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *