【问题标题】:Best practice to return a Result<_, impl Error> and not a Result<_, &str> in Rust?在 Rust 中返回 Result<_, impl Error> 而不是 Result<_, &str> 的最佳实践?
【发布时间】:2019-01-12 11:40:50
【问题描述】:

这种风格Result可以练习吗?

fn a() -> Result<u32, &'static str>

那么 Error trait 的目的是什么? https://doc.rust-lang.org/std/error/trait.Error.html

impl Error Result 是更好的做法吗?

impl Error for MyError {..... }
fn a() -> Result<u32, MyError>

【问题讨论】:

  • 我不知道你为什么认为&amp;str 作为错误类型是好的设计。你读过the relevant section of the book吗?另请参阅C-GOOD-ERR API 指南。第三个问题可能过于主观。 (即使忽略第二行代码的语法错误)
  • 你想要什么,了解这些东西之间的区别?或者只是为了有人告诉你可以在任何地方使用Result&lt;_, &amp;'static str&gt;?错误处理本身可以是一本书(不仅仅是在 Rust 中)。我不认为这个问题可以建设性地回答。
  • 我想我已经得到了答案。这里没有明确的答案。而且我已经很清楚这两种方法之间的权衡。同步错误是故意的伪代码。将更新问题以明确说明

标签: rust


【解决方案1】:

简而言之:不,这不好。 String as error 会丢弃有关详细信息和原因的信息,使调用者无法检查错误并可能从中恢复。

如果您只需要用一些东西填充 Error 参数,请创建一个单元结构。它没有多大用处,但也不像字符串那样易变。您可以轻松区分foo::SomeErrorbar::SomeError

#[derive(Debug)]
pub struct SomeError; // No fields.

如果您可以枚举错误变体,请使用enum。 有时将其他错误“包含”到其中也很有用。

#[derive(Debug)]
pub enum PasswordError {
    Empty,
    ToShort,
    NoDigits,
    NoLetters,
    NoSpecials
}

#[derive(Debug)]
pub enum ConfigLoadError {
   InvalidValues,
   DeserializationError(serde::de::Error),
   IoError(std::io::Error),
}

没有人阻止您使用structs。 当您有意对调用者隐藏某些信息时,它们特别有用(与 enums 的变体始终具有公共可见性相反)。例如。 caller 与错误信息无关,但可以使用kind 处理:

pub enum RegistrationErrorKind {
    InvalidName { wrong_char_idx: usize },
    NonUniqueName,
    WeakPassword,
    DatabaseError(db::Error),
}

#[derive(Debug)]
pub struct RegistrationError {
    message: String, // Private field
    pub kind: RegistrationErrorKind, // Public field
}

impl Error - 存在类型 - 在这里没有意义。如果这是您的意图,您不能在错误位置返回不同的错误类型。而且不透明的错误并没有多大用处,就像字符串一样。

std::error::Error trait 确保您的SomeError 类型具有std::fmt::{Display, Debug} 的实现(相应地用于向用户和开发人员显示错误)并提供一些有用的方法,如source(这将返回此错误的原因); isdowncastdowncast_refdowncast_mut。最后 4 个用于错误类型擦除。

错误类型擦除

错误类型擦除有其权衡,但也值得一提。

在编写一些高级应用程序代码时它也特别有用。但是对于库,在决定使用这种方法之前,您应该三思而后行,因为它会使您的库无法使用“no_std”。

假设您有一些具有非平凡逻辑的函数,它可以返回某些错误类型的值,而不是一个。在这种情况下,您可以使用(但不要滥用)错误类型擦除:

use std::error::Error;
use std::fmt;
use std::fs;
use std::io::Error as IoError;
use std::net::AddrParseError;
use std::net::Ipv4Addr
use std::path::Path;

// Error for case where file contains '127.0.0.1'
#[derive(Debug)]
pub struct AddressIsLocalhostError;

// Display implementation is required for std::error::Error.
impl fmt::Display for AddressIsLocalhostError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Address is localhost")
    }
}

impl Error for AddresIsLocalhostError {} // Defaults are okay here.

// Now we have a function that takes a path and returns 
// non-localhost Ipv4Addr on success.
// On fail it can return either of IoError, AddrParseError or AddressIsLocalhostError.
fn non_localhost_ipv4_from_file(path: &Path) -> Result<Ipv4Addr, Box<dyn Error + 'static>> {
    // Opening and reading file may cause IoError.
    // ? operator will automatically convert it to Box<dyn Error + 'static>. 
    // (via From trait implementation)
    // This way concrete type of error is "erased": we don't know what's
    // in a box, in fact it's kind of black box now, but we still can call
    // methods that Error trait provides.
    let content = fs::read_to_string(path)?;

    // Parsing Ipv4Addr from string [slice] 
    // may cause another error: AddressParseError.
    // And ? will convert it to to the same type: Box<dyn Error + 'static>
    let addr: Ipv4Addr = content.parse()?;

    if addr == Ipv4Add::new(127, 0, 0, 1) {
        // Here we perform manual conversion 
        // from AddressIsLocalhostError 
        // to Box<dyn Error + 'static> and return error.
        return Err(AddressIsLocalhostError.into());
    }

    // Everyhing is okay, returning addr.
    Ok(Ipv4Addr)
}


fn main() {
    // Let's try to use our function.
    let maybe_address = non_localhost_ipv4_from_file(
        "sure_it_contains_localhost.conf"
    );

    // Let's see what kind of magic Error trait provides!
    match maybe_address {
        // Print address on success.
        Ok(addr) => println!("File was containing address: {}", addr),
        Err(err) => {
            // We sure can just print this error with.
            // println!("{}", err.as_ref());
            // Because Error implementation implies Display implementation.
            // But let's imagine we want to inspect error.

            // Here deref coercion implicitly converts
            // `&Box<dyn Error>` to `&dyn Error`.
            // And downcast_ref tries to convert this &dyn Error
            // back to &IoError, returning either
            // Some(&IoError) or none
            if Some(err) = err.downcast_ref::<IoError>() {
                println!("Unfortunately, IO error occured: {}", err)
            }

            // There's also downcast_mut, which does the same, but gives us
            // mutable reference.
            if Some(mut err) = err.downcast_mut::<AddressParseError>() {
                // Here we can mutate err. But we'll only print it.
                println!(
                    "Unfortunately, what file was cantaining, \
                     was not in fact an ipv4 address: {}",
                    err
                );
            }

            // Finally there's 'is' and 'downcast'.
            // 'is' comapres "erased" type with some concrete type.
            if err.is::<AddressIsLocalhostError>() {
               // 'downcast' tries to convert Box<dyn Error + 'static>
               // to box with value of some concrete type.
               // Here - to Box<AddressIsLocalhostError>.
               let err: Box<AddressIsLocalhostError> = 
                   Error::downcast(err).unwrap();
            }
        }
    };
}

总结:错误应该(我会说 - 必须)为调用者提供有用的信息,除了能够显示它们之外,因此它们不应该是字符串。并且错误必须至少实现 Error 以在所有 crate 中保留更不一致的错误处理体验。其余的都视情况而定。

Caio 已经提到了 The Rust Book。

但这些链接也可能有用:

std::any module level API documentation

std::error::Error API documentation

【讨论】:

    【解决方案2】:

    对于简单的用例,像 Result&lt;u32, &amp;'static str&gt;Result&lt;u32, String&gt; 这样的不透明错误类型就足够了,但对于更复杂的库,它很有用,甚至鼓励创建自己的错误类型,如 struct MyErrorenum AnotherLibError ,这可以帮助您更好地定义您的意图。您可能还想阅读Rust by Example 书中的Error Handling 章节。

    Error 特征作为std 的一部分,帮助开发人员以通用和集中的方式定义自己的错误类型,以描述发生的情况和可能的根本原因(回溯)。目前有点受限,但有计划帮助improve its usability

    当您使用impl Error 时,您是在告诉编译器您不关心返回的类型,只要它实现了Error 特征。当错误类型太复杂或想要泛化返回类型时,这种方法很有用。例如:

    fn example() -> Result<Duration, impl Error> {
        let sys_time = SystemTime::now();
        sleep(Duration::from_secs(1));
        let new_sys_time = SystemTime::now();
        sys_time.duration_since(new_sys_time)
    }
    

    duration_since 方法返回 Result&lt;Duration, SystemTimeError&gt; 类型,但在上面的方法签名中,您可以看到对于 Result 的 Err 部分,它返回任何实现 Error 特征的东西。

    总结一切,如果你阅读了 Rust 书并且知道你在做什么,你可以选择最适合你需要的方法。否则,最好为错误定义自己的类型或使用一些第三方实用程序,例如 error-chainfailure crates。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-09-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-01-04
      相关资源
      最近更新 更多