likes
comments
collection
share

Rust 错误处理入门和进阶

作者站长头像
站长
· 阅读数 6

Rust 错误处理入门和进阶

Rust 错误处理入门和进阶

引用 Rust Book 的话,“错误是软件中不可避免的事实”。这篇文章讨论了如何处理它们。

在讨论 可恢复错误和 Result 类型之前,我们首先来谈谈 不可恢复错误 - 又名恐慌(panic)。

不可恢复错误

恐慌(panic)是程序可能抛出的异常。它停止执行当前线程。当发生 恐慌 时,它会返回 错误简短描述以及有关恐慌位置的信息。

fn main() {
    panic!("error!");
    println!("Never reached :(");
}

运行以上代码将会抛出:

thread 'main' panicked at 'error!', examples\panics.rs:2:5

它们与 JavaScript 和其他语言中的 throw 类似,因为它们不需要在函数上添加注释来运行,并且可以穿过函数边界。然而在 Rust 中,恐慌无法恢复,没有办法在当前线程中接收恐慌。

fn send_message(s: String) {
    if s.is_empty() {
       panic!("Cannot send empty message");
    } else {
        // ...
    }
}

send_message 函数是容易出错的(可能会出错)。如果使用 空消息 调用此函数,则程序将停止运行。被调用者无法跟踪已发生的错误。

对于可恢复的错误,Rust 在标准库中有一个用于错误处理的类型,称为 Result 。它是一种通用类型,这意味着result和error变体基本上可以是您想要的任何内容。

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

基本错误处理

目前我们的 send_message 函数没有返回任何内容。这意味着调用方无法接收到任何信息。我们可以更改定义以返回 Result ,而不是恐慌,可以提前返回 Result::Err

fn send_message(s: String) -> Result<(), &'static str> {
    if s.is_empty() {
        // Note the standard prelude includes `Err` so the `Result::Err` and `Err` are equivalent
        return Result::Err("message is empty")
    } else {
        // ...
    }
    Ok(())
}

现在我们的函数实际上返回有关出了什么问题的信息,我们可以在调用它时处理它:

if let Err(send_error) = send_message(message) {
    show_user_error(send_error);
}

处理未使用的结果

在上面的示例中,我们检查项目的值并对其进行分支。然而,如果我们没有检查和处理返回的结果,那么 Rust 编译器会给我们一个有用的警告,这样你就不会忘记显式处理程序中的错误。

|     send_message();
|     ^^^^^^^^^^^^^^^
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled

Result 类型可以在大多数库中找到。我最喜欢的示例之一是 FromStr::from_str trait 方法的返回类型。使用 str::parse (使用 FromStr 特征),我们可以执行以下操作:

fn main() {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).unwrap();

    match input.trim_end().parse::<f64>() {
        Ok(number) => {
            dbg!(number);
        }
        Err(err) => {
            dbg!(err);
        }
    };
}

(暂时忽略 unwrap 😉)

$ cargo r --example input -q
10
[examples\input.rs:7] number = 10.0

$ cargo r --example input -q
100
[examples\input.rs:7] number = 100.0

$ cargo r --example input -q
bad
[examples\input.rs:10] err = ParseFloatError {
    kind: Invalid,
}

在这里我们可以看到,当我们输入一个数字时,我们会得到一个带有数字的 Ok 变体,否则我们会得到一个 ParseFloatError

文件、网络和数据库

当您与外界或 Rust 运行时之外的事物交互时,所有错误都会发生。可能发生大量错误的地方之一是与文件系统的交互。 File::open 函数尝试打开文件。这可能会因多种原因而失败。文件名无效、文件不存在或者您根本没有读取该文件的权限。请注意,这些错误是明确定义的并且是事先已知的。您甚至可以使用 kind 函数访问错误变体,以便实现您的程序逻辑或向用户返回指导性错误消息。

混淆结果和错误

当您从事项目时,您经常会发现自己在函数签名中的返回类型方面重复自己:

fn foo() -> Result<SomeType, MyError> {
...
}

举一个具体的例子,所有操作文件系统的函数都会出现相同的错误(文件不存在、权限无效)。 io::Result 是result的别名,但意味着每个函数不必指定错误类型:

pub type Result<T> = Result<T, io::Error>;

If you have an API which has a common error type, you may want to consider this pattern. 如果您有一个具有常见错误类型的 API,您可能需要考虑此模式。

? 运算符

Results 最好的事情之一是 ? 运算符,? 运算符可以短路 Result 错误值。让我们看一个从文件上传文本的简单函数。这可能会以多种不同的方式出错:

fn upload_file() -> Result<(), &'static str> {
    let text = match std::fs::read_to_string("file.txt").map_err(|_| "read file error") {
        Ok(value) => value,
        Err(err) => {
            return err;
        }
    };
    if let Err(err) = upload_text(text) {
        return err
    }
    Ok(())
}

等等,我们正在写 Rust 而不是 Go!

如果将 ? 后缀到添加到result(或任何实现 try 的东西,也实现 Option ),我们可以获得功能上等效的结果,并且具有更易读和更清晰的结果。简洁的语法。

fn upload_file() -> Result<(), &'static str> {
    let text = std::fs::read_to_string("file.txt").map_err(|_| "read file error")?;
    upload_text(text)?;
    Ok(())
}

只要调用函数也返回具有相同 Error 类型的 Result? 就可以节省大量的显式代码编写。此外,问号在错误值上隐式运行 Into::into (它是为 From 实现者自动实现的)。所以我们在使用运算符之前不必担心转换错误:

// This derive an into implementation for `std::io::Error -> MyError`
#[derive(derive_enum_from_into::EnumFrom)]
enum MyError {
    IoError(std::io::Error)
    // ...
}

fn do_stuff() -> Result<(), MyError> {
    let file = File::open("data.csv")?;
    // ...
}

稍后我们将研究更多组合错误类型的模式!

Error Trait 介绍

Error Trait在标准库中定义。它基本上代表了错误值的期望 - Result<T,E>E 类型的值。 Error Trait针对许多错误实现,并为错误信息提供统一的 API。 Error Trait有点需要,要求错误同时实现 Debug 和 Display。虽然实现起来可能很麻烦,但我们稍后会看到一些工具库来实现这一点。

在标准库中,VarError(用于读取环境变量)和 ParseIntError(用于将字符串切片解析为整数)是不同的错误。当我们与它们交互时,我们需要区分类型,因为它们具有不同的属性和不同的堆栈大小。为了构建它们的组合,我们可以使用枚举构建一个总和类型。或者,我们可以使用动态分派的特征来处理不同的堆栈大小的项目和其他类型信息。

使用上面提到的 try 语法 ( ? ),我们可以将上面的错误转换为动态派发。这使得处理不同的错误变得容易,而无需构建枚举来组合错误。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let key = std::env::var("NUMBER_IN_ENV")?;
    let number = key.parse::<i32>()?;
    println!("\"NUMBER_IN_ENV\" is {}", number);
    Ok(())
}

虽然这是处理错误的简单方法,但区分类型并不容易,并且可能会使库中的错误处理变得困难。稍后会详细介绍这一点。

错误特征与结果枚举

使用枚举时的一件事是我们可以使用 match 在枚举错误变体上进行分支。另一方面,对于 dyn 特征,除非您沿着向下转换路径,否则很难获得有关错误的具体信息:

match my_enum_error {
    FsError(err) => {
        report_fs_error(err)
    },
    DbError(DbError { err, database }) => {
        report_db_error(database, err)
    },
}

对于可重用的库,最好使用枚举来组合错误,以便库的用户可以自己处理具体细节。但对于 CLI 和其他应用程序来说,使用该特征可能要简单得多。

Methods on Result

结果和选项包含许多有用的功能。以下是我常用的一些功能:

Result::map()

Result::map 映射或转换 Ok 值(如果存在)。这比使用 ? 运算符更简洁。

fn string_to_plus_one(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>().map(|num| num + 1)
}

Result::ok()

Result::ok 对于将结果转换为选项很有用

assert_eq!(Ok(2).ok(), Some(2));
assert_eq!(Err("err!").ok(), None);

Option::ok_or_else()

Option::ok_or_else 对于从选项转换为结果的另一种方式很有用

fn get_first(vec: &Vec<i32>) -> Result<&i32, NotInVec> {
    vec.first().ok_or_else(|| NotInVec)
}

迭代中的错误处理

在迭代器链中使用Result可能会有点令人困惑。幸运的是 Result 实现了collect。如果发生错误,我们可以使用它来提前结束迭代器。在下面,如果所有 parse 都成功,那么我们将获得收集的数字结果向量。如果失败,那么它会返回一个带有失败错误的结果。

fn main() {
    let a = ["1", "2", "not a number"]
        .into_iter()
        .map(|a| a.parse::<f64>())
        .collect::<Result<Vec<_>, _>>();
    dbg!(a);
}
[examples\iteration.rs:6] a = Err( ParseFloatError { kind: Invalid, }, )

删除 "not a number" 条目后

[examples\iteration.rs:3] a = Ok( [ 1.0, 2.0, ], )

因为 Rust 迭代器是分段且惰性的,所以迭代器可能会短路,而无需对任何后续项进行解析。

更多Panic

特殊panics

todo!()unimplemented!()unreachable!() 都是 panic! () 的包装器,但专门针对其情况。 Panics 有一个特殊的 ! 类型,称为“never type”,它表示永远不会完成的计算结果(也意味着它可以传递到任何地方):

fn func_i_havent_written_yet() -> u32 {
    todo!()
}

有时,编译器无法正确推断某些 Rust 代码是否有效。对于这种情况,可以使用 unreachable! 恐慌:

fn get_from_vec_else_zero(a: Vec<i32>) -> i32 {
    if let Some(value) = a.get(2) {
        if let Some(prev_value) = a.get(1) {
            prev_value
        } else {
            unreachable!()
        }
    } else {
        0
    }
}

Unwrapping

unwrapResultOption 上的方法。他们返回 OkSome 变体,否则会出现恐慌......

// result.unwrap()

let value = if let Ok(value) = result {
    value
} else {
    panic!("Unwrapped!")
};

其用例是开发人员错误和编译器无法完全弄清楚的情况。如果您只是尝试某些操作并且不想设置完整的错误处理系统,那么它们可以用于忽略编译器警告。

即使情况需要 unwrap ,您最好使用 expect ,它附带一条消息 - 当出现 expect 错误消息时,您会感谢过去的自己帮助您在两周后找到问题的根本原因

标准库中的恐慌 Panics

值得注意的是,标准库中的某些 API 可能会出现恐慌。您应该在文档中查找这些注释。其中之一是 Vec::remove。如果您使用它,您应该确保参数位于其可索引范围内。

fn remove_at_idx(a: usize, vec: &mut Vec<i32>) -> Option<i32> {
    if a < idx.len() {
        Some(vec.remove(a))
    } else {
        None
    }
}

处理多个错误和 工具 Crates

处理来自多个库和 API 的错误可能会变得具有挑战性,因为您必须处理一堆不同类型的错误。它们的大小不同,包含不同的信息。为了统一类型,我们必须使用枚举构建一个总和类型,以确保它们在编译时具有相同的大小。

enum Errors {
    FileSystemError(..),
    StringParseError(..),
    NetworkError(..),
}

一些使创建这些统一枚举更容易的包:

thiserror crate

thiserror 提供了一个派生实现,为我们添加了 Error 特征。如前所述,要实现错误,我们必须实现显示,并且 thiserrors 的 #[error] 属性为显示的错误提供模板。

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error("unknown data store error")]
    Unknown,
}

anyhow crate

anyhow 提供了一种符合人体工程学且惯用的方法来显式处理错误。它与前面提到的错误特征类似,但具有附加功能,例如为抛出的错误添加上下文。

当您想以上下文感知的方式向应用程序的用户传达错误时,这确实非常有用:

use anyhow::{bail, Result, Context};

fn main() -> Result<()> {
    println!("Hello World!");
    func1().context("while calling func1")?;
    Ok(())
}

fn func1() -> Result<()> {
    func2().context("while calling func2")
}

fn func2() -> Result<()> {
    bail!("Hmm something went wrong ")
}
Error: while calling func1

Caused by:
    0: while calling func2
    1: Hmm something went wrong

Error 特征类似, anyhow 也会遇到无法匹配 anyhow 的结果错误变体的问题。这就是为什么 anyhow 的文档建议对应用程序使用 anyhow ,对库使用 thiserror

eyre crate

最后, eyreanyhow 的分支,并添加了更多回溯信息。它是高度可定制的,并且使用 color-eyre 我们可以在恐慌消息中获得颜色 - 一点颜色总是可以照亮开发体验。

The application panicked (crashed).
Message:  test
Location: examples\color_eyre.rs:6

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13 frames hidden ⋮
  14: core::ops::function::FnOnce::call_once<enum$<core::result::Result<tuple$<>,eyre::Report>, 1, 18446744073709551615, Err> (*)(),tuple$<> ><unknown>
      at /rustc/7737e0b5c4103216d6fd8cf941b7ab9bdbaace7c\library\core\src\ops\function.rs:227
17 frames hidden ⋮

尾声

感谢您阅读这篇文章!错误处理可能很困难,但通过本指南,希望您能更好地了解如何确保能够可靠地跟踪错误并使调试 Rust 应用程序变得更加容易。


原文地址:More than you've ever wanted to know about errors in Rust