Вы забудете о необработанных исключениях, если научитесь обращаться с ошибками, как это принято в Rust
Обработка ошибок – важнейшая часть любого языка программирования, а в Rust предлагается для этого уникальный мощный подход. Rust, в отличие от многих других языков программирования, не предлагает исключений, а предоставляет перечисление Result. У разработчика нет иного выхода, кроме как обрабатывать все ошибки согласованно и предсказуемо. Поэтому ошибки становится проще идентифицировать и диагностировать.
Поскольку в Rust не предусмотрены исключения, любая функция обязана возвращать либо значение, либо «панику«. Когда функция паникует, процесс сразу же завершается и предоставляет вызывающей стороне конкретную обратную связь. Чисто технически можно отлавливать случаи паники в Rust при помощи catch_unwind, но постоянно так делать не рекомендуется. Вместо этого в Rust предлагается перечисление Result; пользуясь им, разработчик вынужден обрабатывать все ошибки сам.
В этом посте будут рассмотрены применяемые в Rust идиоматические паттерны обработки ошибок, и вы в общих чертах поймёте, как это делается. Мы разберём перечисление Result, посмотрим, как с его помощью обрабатываются ошибки в программах Rust, и какие крейты чаще всего используются, чтобы упростить этот процесс.
Тип 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/

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