#rust #error-handling #traits
Вопрос:
Введение
Я изучаю rust и пытаюсь найти правильную подпись для использования нескольких результатов в одной функции, а затем возвращаю либо правильное значение, либо выхожу из программы с сообщением.
До сих пор у меня есть 2 разных метода, и я пытаюсь их объединить.
Контекст
Это то, чего я пытаюсь достичь:
fn blur(image: DynamicImage, amount: amp;str) -> DynamicImage {
let amount = parse_between_or_error_out("blur", amount, 0.0, 10.0);
image.brighten(amount)
}
Это то, над чем я сейчас работаю, но хотел бы провести рефакторинг.
fn blur(image: DynamicImage, amount: amp;str) -> DynamicImage {
match parse::<f32>(amount) {
Ok(amount) => {
verify_that_value_is_between("blur", amount, 0.0, 10.0);
image.blur(amount)
}
_ => {
println!("Error");
process::exit(1)
}
}
}
Объединение этих методов
Теперь вот два рабочих метода, которые я пытаюсь объединить, чтобы достичь этого.
fn parse<T: FromStr>(value: amp;str) -> Result<T, <T as FromStr>::Err> {
value.parse::<T>()
}
fn verify_that_value_is_between<T: PartialOrd std::fmt::Display>(
name: amp;str,
amount: T,
minimum: T,
maximum: T,
) {
if amount > maximum || amount < minimum {
println!(
"Error: Expected {} amount to be between {} and {}",
name, minimum, maximum
);
process::exit(1)
};
println!("- Using {} of {:.1}/{}", name, amount, maximum);
}
Вот что я попробовал
Я попробовал следующее. Я понимаю, что, скорее всего, делаю целый ряд вещей неправильно. Это потому, что я все еще изучаю Rust, и я хотел бы получить любую обратную связь, которая поможет мне научиться совершенствоваться.
fn parse_between_or_error_out<T: PartialOrd FromStr std::fmt::Display>(
name: amp;str,
amount: amp;str,
minimum: T,
maximum: T,
) -> Result<T, <T as FromStr>::Err> {
fn error_and_exit() {
println!(
"Error: Expected {} amount to be between {} and {}",
name, minimum, maximum
);
process::exit(1);
}
match amount.parse::<T>() {
Ok(amount) => {
if amount > maximum || amount < minimum {
error_and_exit();
};
println!("- Using {} of {:.1}/{}", name, amount, maximum);
amount
}
_ => {
error_and_exit();
}
}
}
В настоящее время это выглядит довольно беспорядочно, вероятно, я использую слишком много или неправильные типы, и ошибка должна быть в двух местах (отсюда встроенная функция, которая, как я знаю, не является хорошей практикой).
Полный воспроизводимый пример.
Вопрос
Как лучше всего объединить логику, использующую результат и другое условие (или результат), выйти с сообщением или дать T
в результате?
Комментарии по поводу любых допущенных ошибок тоже очень приветствуются.
Комментарии:
1. Полный ответ был бы длинным и, вероятно, уже где-то есть, так что вопрос, вероятно, будет продублирован на какой-нибудь не очень очевидный другой. Что я хотел бы отметить: 1) Если вы заботитесь о масштабируемости кода, не паникуйте по всем ошибкам, возвращайте их в свой
Result
s; 2)?
иimpl From<XXXError> for MyError
будьте вашими друзьями. Вот статья об обработке ошибок , в которой упоминается все это.2. Спасибо. Я искал ТАК и не смог найти никакого ответа, который бы касался того, какую подпись наиболее эффективно использовать в подобных случаях. Я был бы рад короткому ответу, который показывает более чистую подпись, которую также можно использовать в качестве примера. Я считаю, что это может быть полезно и для других при изучении rust, так как поначалу это не совсем прямолинейно.
3. Нет веской причины для использования
amp;str
amount
— просто используйте правильный тип. Это кодовый запах под названием «примитивная одержимость».4. @SvetlinZarev Вы имеете в виду замену его строкой? Если вы имеете в виду i32 или f32, то не могли бы вы уточнить, как вы дедуплицируете синтаксический анализ строки, которая является аргументом cli, для i32 или f32 соответственно? Я нашел макрос в clap (парсер аргументов cli) value_t , но у него есть подпись результата, и это то, что я пытаюсь объединить в 1 методе. Просто пытаюсь научиться правильному пути здесь 🙂
Ответ №1:
Вы можете использовать ящик, такой как anyhow, чтобы создавать пузырьки для ваших событий и обрабатывать их по мере необходимости.
В качестве альтернативы вы можете написать свою собственную черту и реализовать ее Result
.
trait PrintAndExit<T> {
fn or_print_and_exit(amp;self) -> T;
}
Затем используйте его, вызвав метод любого типа, который его реализует:
fn try_get_value() -> Result<bool, MyError> {
MyError { msg: "Something went wrong".to_string() }
}
let some_result: Result<bool, MyError> = try_get_value();
let value: bool = some_result.or_print_and_exit();
// Exits with message: "Error: Something went wrong"
Реализацию этой черты Result
можно было бы сделать с помощью:
struct MyError {
msg: String,
}
impl<T> PrintAndExit<T> for Result<T, MyError> {
fn or_print_and_exit(amp;self) -> T {
match self {
Ok(val) => val,
Err(e) => {
println!("Error: {}", e.msg);
std::process::exit(1);
},
}
}
}
Комментарии:
1. Это именно то, чего я пытался достичь, и я забыл все, что узнал о чертах ржавчины, когда пытался ее решить. Большое спасибо.
Ответ №2:
Вот несколько СУХИХ трюков.
tl;dr:
- Преобразуйте другие ошибки в свой унифицированный тип(ы) ошибок с помощью
impl From<ExxError> for MyError
; - В любой функции, которая может привести к ошибке, используйте
?
как можно больше. ReturnResult<???, MyError>
(*).?
будет использовать неявное преобразование.
(*) Только в том случае, если MyError является подходящим типом для функции. Всегда создавайте или используйте наиболее подходящие типы ошибок. (Вроде бы очевидно, но люди часто относятся к типам ошибок как к коду второго класса, предназначенному для каламбура)
Рекомендации содержатся в комментариях.
use std::error::Error;
use std::str::FromStr;
// Debug and Display are required by "impl Error" below.
#[derive(Debug)]
enum ProcessingError {
NumberFormat{ message: String },
NumberRange{ message: String },
ProcessingError{ message: String },
}
// Display will be used when the error is printed.
// No need to litter the business logic with error
// formatting code.
impl Display for ProcessingError {
fn fmt(amp;self, f: amp;mut Formatter<'_>) -> std::fmt::Result {
match self {
ProcessingError::NumberFormat { message } =>
write!(f, "Number format error: {}", message),
ProcessingError::NumberRange { message } =>
write!(f, "Number range error: {}", message),
ProcessingError::ProcessingError { message } =>
write!(f, "Image processing error: {}", message),
}
}
}
impl Error for ProcessingError {}
// FromStr::Err will be implicitly converted into ProcessingError,
// when ProcessingError is needed. I guess this is what
// anyhow::Error does under the hood.
// Implement From<X> for ProcessingError for every X error type
// that your functions like process_image() may encounter.
impl From<FromStr::Err> for ProcessingError {
fn from(e: FromStr::Err) -> ProcessingError {
ProcessingError::NumberFormat { message: format!("{}", e) }
}
}
pub fn try_parse<T: FromStr>(value: amp;str) -> Result<T, ProcessingError> {
// Note ?. It will implicitly return
// Err(ProcessingError created from FromStr::Err)
Ok (
value.parse::<T>()?
)
}
// Now, we can have each function only report/handle errors that
// are relevant to it. ? magically eliminates meaningless code like
// match x { ..., Err(e) => Err(e) }.
pub fn parse_between<T>(value: amp;str, min_amount: T, max_amount: T)
-> Result<T, ProcessingError>
where
T: FromStr PartialOrd std::fmt::Display,
{
let amount = try_parse::<T>(value)?;
if amount > max_amount || amount < min_amount {
Err(ProcessingError::NumberRange {
message: format!(
"Expected value to be between {} and {} but received {}",
min_amount,
max_amount,
amount)
})
} else {
Ok(amount)
}
}
main.rs
use image::{DynamicImage};
use std::fmt::{Debug, Formatter, Display};
fn blur(image: DynamicImage, value: amp;str)
-> Result<DynamicImage, ProcessingError>
{
let min_amount = 0.0;
let max_amount = 10.0;
// Again, note ? in the end.
let amount = parse_between(value, min_amount, max_amount)?;
image.blur(amount)
}
// All processing extracted into a function, whose Error
// then can be handled by main().
fn process_image(image: DynamicImage, value: amp;str)
-> Result<DynamicImage, ProcessingError>
{
println!("applying blur {:.1}/{:.1}...", amount, max_amount);
image = blur(image, value);
// save image ...
image
}
fn main() {
let mut image = DynamicImage::new(...);
image = match process_image(image, "1") {
Ok(image) => image,
// No need to reuse print-and-exit functionality. I doubt
// you want to reuse it a lot.
// If you do, and then change your mind, you will have to
// root it out of all corners of your code. Better return a
// Result and let the caller decide what to do with errors.
// Here's a single point to process errors and exit() or do
// something else.
Err(e) => {
println!("Error processing image: {:?}", e);
std::process::exit(1);
}
}
}
Комментарии:
1. Хотя это еще один шаг в сложности и немного более сложный для начинающих, это действительно, кажется, делает код более управляемым. Переварив концепции других ответов, это отличный следующий шаг. Большое вам спасибо за дополнительную информацию.
Ответ №3:
Делюсь своими результатами
Я также поделюсь своими результатами/ответом для других людей, которые новички в Rust. Этот ответ основан на ответе @Acidic9.
- Типы, кажется, в порядке
- во всяком случае, похоже, что это фактический стандарт в Rust.
- Я должен был использовать признак и реализовать этот признак для типа ошибки.
Я считаю, что приведенный ниже пример близок к тому, как он может выглядеть в дикой природе.
// main.rs
use image::{DynamicImage};
use app::{parse_between, PrintAndExit};
fn main() {
// mut image = ...
image = blur(image, "1")
// save image
}
fn blur(image: DynamicImage, value: amp;str) -> DynamicImage {
let min_amount = 0.0;
let max_amount = 10.0;
match parse_between(value, min_amount, max_amount).context("Input error") {
Ok(amount) => {
println!("applying blur {:.1}/{:.1}...", amount, max_amount);
image.blur(amount)
}
Err(error) => error.print_and_exit(),
}
}
И реализация внутри библиотеки приложений, во всяком случае, с использованием.
// lib.rs
use anyhow::{anyhow, Error, Result};
use std::str::FromStr;
pub trait Exit {
fn print_and_exit(self) -> !;
}
impl Exit for Error {
fn print_and_exit(self) -> ! {
eprintln!("{:#}", self);
std::process::exit(1);
}
}
pub fn try_parse<T: FromStr>(value: amp;str) -> Result<T, Error> {
match value.parse::<T>() {
Ok(value) => Ok(value),
Err(_) => Err(anyhow!(""{}" is not a valid value.", value)),
}
}
pub fn parse_between<T>(value: amp;str, min_amount: T, max_amount: T) -> Result<T, Error>
where
T: FromStr PartialOrd std::fmt::Display,
{
match try_parse::<T>(value) {
Ok(amount) => {
if amount > max_amount || amount < min_amount {
return Err(anyhow!(
"Expected value to be between {} and {} but received {}",
min_amount,
max_amount,
amount
));
};
Ok(amount)
}
Err(error) => Err(error),
}
}
Надеюсь, что эта полная реализация поможет кому-то там.