Rust 过错处理入门和进阶

Rust 过错处理入门和进阶

引用 Rust Book 的话,“过错是软件中不行避免的事实”。这篇文章讨论了怎么处理它们。

在讨论 可康复过错和 Result 类型之前,咱们首先来谈谈 不行康复过错 – 又名惊惧(panic)。

不行康复过错

惊惧(panic)是程序或许抛出的反常。它中止执行当时线程。当产生 惊惧 时,它会回来 过错简短描述以及有关惊惧位置的信息。

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

运转以上代码将会抛出:

thread 'main' panicked at 'error!', examplespanics.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
[examplesinput.rs:7] number = 10.0
$ cargo r --example input -q
100
[examplesinput.rs:7] number = 100.0
$ cargo r --example input -q
bad
[examplesinput.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);
}
[examplesiteration.rs:6] a = Err( ParseFloatError { kind: Invalid, }, )

删除 "not a number" 条目后

[examplesiteration.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: examplescolor_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/7737e0b5c4103216d6fd8cf941b7ab9bdbaace7clibrarycoresrcopsfunction.rs:22717 frames hidden ⋮

尾声

感谢您阅览这篇文章!过错处理或许很困难,但经过本指南,期望您能更好地了解怎么保证可以可靠地跟踪过错并使调试 Rust 应用程序变得更加简单。


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