Rust错误处理入门指南

作者 : IT 大叔 本文共6617个字,预计阅读时间需要17分钟 发布时间: 2021-04-7

Rust编程语言中的示例项目非常适合向Rust的不同方面和功能引入新的Rustaceans。在本文中,我们将研究通过扩展Rust编程语言中minigrep项目来实现更健壮的错误处理基础结构的一些不同方法。

minigrep项目在第12章中介绍,并引导读者构建grep命令行工具的简单版本,该工具是用于搜索文本的实用程序。例如,您要传递查询,要搜索的文本以及文本所在的文件名,然后取回包含查询文本的所有行。

这篇文章的目的是minigrep通过更健壮的错误处理模式来扩展本书的实现,以便您更好地了解Rust项目中处理错误的不同方法。

作为参考,您可以在minigrep 此处找到本书版本的最终代码。

错误处理用例

构建Rust项目时,常见的模式是有一个“库”部分,其中存放了主要的数据结构,功能和逻辑,还有一个“应用程序”部分,将库的功能联系在一起。

您可以在原始minigrep代码的文件结构中看到这一点:应用程序逻辑位于src/bin/main.rs文件内部,并且仅是src/lib.rs文件中定义的数据结构和函数的一个薄包装。该main函数所做的全部就是call minigrep::run

必须指出这一点很重要,因为取决于我们是构建应用程序还是库,这将改变我们处理错误处理的方式。

当涉及到一个应用程序时,最终用户很可能不想知道导致错误的原因的细节。实际上,只有在错误不可恢复的情况下,才应该将错误通知给应用程序的最终用户。在这种情况下,提供有关为什么发生不可恢复错误的详细信息也很有用,尤其是在与用户输入有关的情况下。如果在后台发生某种可恢复的错误,则应用程序的使用者可能不需要了解它。

相反,当涉及到库时,最终用户是正在使用该库并在其之上构建内容的其他开发人员。在这种情况下,我们希望提供有关库中发生的任何错误的尽可能多的相关详细信息。然后,库的使用者将决定他们如何处理这些错误。

那么,当我们在项目中同时拥有库部分和应用程序部分时,这两种方法如何一起发挥作用?该main函数执行该minigrep::run函数,并输出由此而出现的所有错误。因此,我们大多数错误处理工作都将集中在库部分。

出现库错误

在中src/lib.rs,我们有两个函数Config::newrun,它们可能会返回错误:

impl Config {
    pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
        args.next();

    let query = match args.next() {
        Some(arg) => arg,
        None => return Err("Didn't get a query string"),
    };

    let filename = match args.next() {
        Some(arg) => arg,
        None => return Err("Didn't get a file name"),
    };

    let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

    Ok(Config {
        query,
        filename,
        case_sensitive,
    })
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

恰好有三个地方将返回错误:Config::new函数中发生两个错误,该错误返回a Result<Config, &'static str>。在这种情况下,的错误变量Result是静态字符串切片。

如果用户未提供查询,我们将在此处返回错误。

let query = match args.next() {
    Some(arg) => arg,
    None => return Err("Didn't get a query string"),
};

如果用户未提供文件名,我们将在此处返回错误。

let filename = match args.next() {
    Some(arg) => arg,
    None => return Err("Didn't get a file name"),
};

以这种方式将错误构造为静态字符串的主要问题是错误消息不在中央位置,因此我们可以根据需要轻松地重构它们。这也使保持我们的错误消息在相同类型的错误之间保持一致变得更加困难。

第三个错误发生在run函数顶部,该错误返回a Result<(), Box<dyn Error>>。在这种情况下,错误变量是实现traittrait对象。换句话说,此函数的错误变量是实现特征的类型的任何实例。Error Error

在这里,我们冒泡了由于调用而可能发生的任何错误fs::read_to_string

let contents = fs::read_to_string(config.filename)?;

这适用于可能因调用而出现的错误,fs::read_to_string因为此函数能够返回多种类型的错误。因此,我们需要一种方法来一般性地表示这些不同的可能的错误类型。它们之间的共同点是它们都实现了Error特质!

最终,我们要做的是在中央位置定义所有这些不同类型的错误,并将它们全部作为单一类型的变体。

在中央类型中定义错误变体

我们将创建一个新src/error.rs文件并定义一个枚举AppErrorDebug在过程中派生此特征,以​​便在需要时可以获取调试表示。我们将以这种方式命名该枚举的每个变体,以使其分别代表三种类型的错误:

#[derive(Debug)]
pub enum AppError {
    MissingQuery,
    MissingFilename,
    ConfigLoad,
}

第三个变体ConfigLoad映射到fs::read_to_stringConfig::run函数中调用时可能出现的错误。起初这似乎有点不合时宜,因为如果该函数发生错误,那么读取提供的配置文件将是某种I / O问题。那么,为什么不命名呢IOError

在这种情况下,由于我们要显示标准库函数中的错误,因此与应用程序更相关的是描述表面错误如何影响它,而不是简单地重复它。当发生错误时fs::read_to_string,这会阻止我们Config进行加载,因此这就是我们将其命名的原因ConfigLoad

现在我们有了这种类型,我们需要更新代码中所有返回错误的地方,以利用此AppError枚举。

我们的退货变体 AppError

src/lib.rs文件的顶部,我们需要声明我们的error模块并将其error::AppError纳入范围:

mod error;

use error::AppError;

在我们的Config::new函数中,我们需要更新将静态字符串切片作为错误返回的位置以及函数本身的返回类型:

- pub fn new(mut args: env::Args) -> Result<Config, &'static str>
+ pub fn new(mut args: env::Args) -> Result<Config, AppError>
    // --snip--

    let query = match args.next() {
        Some(arg) => arg,
-       None => return Err("Didn't get a query string"),
+       None => return Err(AppError::MissingQuery),
    };

    let filename = match args.next() {
        Some(arg) => arg,
-       None => return Err("Didn't get a file name"),
+       None => return Err(AppError::MissingFilename),
    };

    // --snip--

run函数中的第三个错误仅需要我们更新其返回类型,因为?操作员已经在负责将错误冒泡并在发生错误时将其返回。

- pub fn run(config: Config) -> Result<(), Box<dyn Error>>
+ pub fn run(config: Config) -> Result<(), AppError>

好的,所以我们现在要利用我们的错误变量,这些错误变量应该在我们的main函数中浮出水面并打印出来。但是我们不再拥有以前在任何地方定义的实际错误消息!

注释错误变体 thiserror

所述thiserror 是一个通常用于提供符合人体工程学的方式来在一个锈病库格式错误消息。

它允许我们使用AppError要显示给最终用户的实际错误消息来注释枚举中的每个变体。

让我们将其作为依赖项添加到我们的Cargo.toml中:

[dependencies]
thiserror = "1"

在本文中,src/error.rs我们将thiserror::Error特征纳入范围并让我们的AppError类型派生它。我们需要派生此特征,以便用一个#[error]块注释每个枚举变量。现在,我们指定要针对每个特定变体显示的错误消息:

+ use std::io;

+ use thiserror::Error;

- #[derive(Debug)]
+ #[derive(Debug, Error)]
pub enum AppError {
+   #[error("Didn't get a query string")]
    MissingQuery,
+   #[error("Didn't get a file name")]
    MissingFilename,
+   #[error("Could not load config")]
    ConfigLoad {
+       #[from] 
+       source: io::Error,
+   }
}

什么是所有额外的东西添加到ConfigLoad变体?由于ConfigLoad仅当对的调用存在基础错误时才会发生错误,所以fs::read_to_stringConfigLoad变体实际上在做的事情是围绕基础I / O错误提供了额外的上下文。

thiserror允许我们通过在附加注释中包装一个较低级别的错误#[from],以便将转换source为我们的自制错误类型。这样,当发生I / O错误时(例如当我们指定要搜索的文件实际上不存在时),我们将得到如下错误:

Could not load config: Os { code: 2, kind: NotFound, message: "No such file or directory" }

没有它,产生的错误消息如下所示:

Os { code: 2, kind: NotFound, message: "No such file or directory" }

对于我们图书馆的使用者来说,更难弄清这个错误的根源。额外的上下文有很大帮助。

你可以找到的版本minigrep使用thiserror 在这里

更手动的方法

现在,我们将切换齿轮,并研究如何实现与我们相同的结果thiserror,但又不会依赖它。

在后台,thiserror使用程序宏执行一些魔术操作,这可以对编译速度产生显着影响。在的情况下minigrep,我们的错误变体很少,并且项目很小,因此对依赖的依赖thiserror不会真正增加编译时间,但是在更大,更复杂的项目中可能需要考虑这一点。

因此,在该注释上,我们将通过摘录并将其替换为我们自己的手动实现来结束这篇文章。沿这条路线走的好处是,我们只需要对src/error.rs文件进行更改即可实现所有必要的更改(当然,当然thiserror还要从Cargo.toml中删除)。

[dependencies]
- thiserror = "1"

让我们删除所有thiserror提供给我们的注释。我们还将用thiserror::Errortrait替换std::error::Errortrait:

- use thiserror::Error;
+ use std::error::Error;

- #[derive(Debug, Error)]
+ #[derive(Error)]
pub enum AppError {
-   #[error("Didn't get a query string")]
    MissingQuery,
-   #[error("Didn't get a file name")]
    MissingFilename,
-   #[error("Could not load config")]
    ConfigLoad {
-      #[from]
       source: io::Error,
    }
}

为了取回我们刚刚擦除的所有功能,我们需要做三件事:

  1. 实现的Display 特征AppError以便我们的错误变体可以显示给用户。
  2. 落实Error 特质AppError。此特征表示错误类型的基本期望,即它们实现DisplayDebug,还具有获取错误的根本原因或原因的能力。
  3. 实现From<io::Error>AppError。这是必需的,这样我们才能将从返回的I / O错误转换fs::read_to_string为的实例AppError

这是我们对Display特质的实现AppError。它将每个错误变量映射到一个字符串,并将其写入Display格式化程序。

use std::fmt;

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MissingQuery => f.write_str("Didn't get a query string"),
            Self::MissingFilename => f.write_str("Didn't get a file name"),
            Self::ConfigLoad { source } => write!(f, "Could not load config: {}", source),
        }
    }
}

这是我们对Error特质的实现。要实现的主要方法是Error::source方法,该方法旨在提供有关错误源的信息。对于我们的AppError类型,仅ConfigLoad公开任何基础源信息,即,由于调用可能发生的I / O错误fs::read_to_string。在其他错误变体的情况下,没有公开的基础源信息。

use std::error;

impl error::Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::ConfigLoad { source } => Some(source),
            _ => None,
        }
    }
}

&(dyn Error + 'static)返回类型的一部分类似于Box<dyn Error>我们之前看到的trait对象。这里的主要区别是trait对象位于不可变引用的后面,而不是Box指针的后面。'static这里的生存期表示特征对象本身仅包含拥有的值,即,它在内部不存储任何引用。为了使编译器确信这里没有悬挂指针的机会,这是必需的。

最后,我们需要一种将转换io::Error为的方法AppError。我们将通过实现来做到这一点From<io::error> for AppError

impl From<io::Error> for AppError {
    fn from(source: io::Error) -> Self {
        Self::ConfigLoad { source }
    }
}

这个没有太多。如果得到an io::Error,我们将其转换为an的全部工作AppError就是将其包装在一个ConfigLoad变体中。

这就是所有的人!您可以在此处找到此minigrep实现的版本。

概括

最后,我们讨论了错误处理部门中Rust编程语言书中minigrep介绍的原始实现是如何缺少的,以及如何考虑不同的错误处理用例。

从那里开始,我们展示了如何使用thiserror板条箱将所有可能的错误变体集中到一个类型中。

最后,我们剥离了thiserror提供的饰面,并展示了如何手动复制相同的功能。

免责声明:
1. 本站资源转自互联网,源码资源分享仅供交流学习,下载后切勿用于商业用途,否则开发者追究责任与本站无关!
2. 本站使用「署名 4.0 国际」创作协议,可自由转载、引用,但需署名原版权作者且注明文章出处
3. 未登录无法下载,登录使用金币下载所有资源。
IT小站 » Rust错误处理入门指南

常见问题FAQ

没有金币/金币不足 怎么办?
本站已开通每日签到送金币,每日签到赠送五枚金币,金币可累积。
所有资源普通会员都能下载吗?
本站所有资源普通会员都可以下载,需要消耗金币下载的白金会员资源,通过每日签到,即可获取免费金币,金币可累积使用。

发表评论