使用多个条件或结果时,哪个签名最有效?如何正确冒泡错误?

Which signature is most effective when using multiple conditions or Results? How to bubble errors correctly?

简介

我正在学习 Rust 并一直在尝试找到正确的签名,以便在单个函数中使用多个结果,然后返回正确的值,或者通过消息退出程序。

到目前为止,我有 2 种不同的方法,我正在尝试将它们结合起来。

上下文

这就是我想要实现的目标:

fn blur(image: DynamicImage, amount: &str) -> DynamicImage {
    let amount = parse_between_or_error_out("blur", amount, 0.0, 10.0);
    image.brighten(amount)
}

这是我现在的工作,但想重构。

fn blur(image: DynamicImage, amount: &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: &str) -> Result<T, <T as FromStr>::Err> {
    value.parse::<T>()
}
fn verify_that_value_is_between<T: PartialOrd + std::fmt::Display>(
    name: &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: &str,
    amount: &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();
        }
    }
}

目前这看起来很乱,可能我使用了太多或错误的类型并且错误需要出现在两个地方(因此内联函数,我知道这不是好的做法)。

Full reproducible example.

问题

如何最好地结合使用结果和另一个条件(或结果)的逻辑,以消息退出或给出 T 作为结果?

也非常欢迎对所犯的任何错误发表评论。

您可以使用诸如 anyhow 之类的 crate 来冒泡您的事件并根据需要处理它们。

或者,您可以编写自己的特征并在 Result 上实现它。

trait PrintAndExit<T> {
  fn or_print_and_exit(&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(&self) -> T {
        match self {
            Ok(val) => val,
            Err(e) => {
                println!("Error: {}", e.msg);
                std::process::exit(1);
            },
        }
    }
}

分享我的结果

我也会与其他 Rust 新手分享我的 results/answer。此答案基于@Acidic9 的答案。

  • 类型好像没问题
  • anyhow 看起来是 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: &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: &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: &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),
    }
}

希望看到这个完整的实现能对那里的人有所帮助。

Source code.

这里有一些 DRY 技巧。

tl;博士:

  1. 使用 impl From<ExxError> for MyError;
  2. 将其他错误转换为您的统一错误类型
  3. 在任何可能导致错误的函数中,尽可能使用?。 Return Result<???, MyError> (*)。 ? 将利用隐式转换。

(*) 仅当 MyError 是函数的适当类型时。始终创建或使用最合适的错误类型。 (有点明显,但人们通常将错误类型视为第二个 class 代码,双关语)

建议在评论里。

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(&self, f: &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: &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: &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: &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: &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);
        }
    }
}