Какая подпись наиболее эффективна при использовании нескольких условий или результатов? Как правильно выявлять ошибки?

#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:

  1. Преобразуйте другие ошибки в свой унифицированный тип(ы) ошибок с помощью impl From<ExxError> for MyError ;
  2. В любой функции, которая может привести к ошибке, используйте ? как можно больше. Return Result<???, 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),
    }
}
 

Надеюсь, что эта полная реализация поможет кому-то там.

Исходный код.