更友好的错误报告

我们只能接受错误会发生的事实。与许多其他语言相比,在使用 Rust 时很难忽视和处理这种现实:因为 Rust 没有异常,所以所有可能的错误状态通常都编码在函数的返回类型中。

结果

read_to_string 这样的函数不会返回字符串。相反,它返回一个 Result,其中包含一个 String 或某种类型的错误(在本例中为 std::io::Error)。

你怎么知道是哪一个?由于 Result 是一个 enum,您可以使用 match 来检查它是哪种变体


#![allow(unused)]
fn main() {
let result = std::fs::read_to_string("test.txt");
match result {
    Ok(content) => { println!("File content: {}", content); }
    Err(error) => { println!("Oh noes: {}", error); }
}
}

解包

现在,我们能够访问文件的内容,但在 match 块之后我们无法真正对它做任何事情。为此,我们需要以某种方式处理错误情况。挑战在于 match 块的所有分支都需要返回相同类型的某些内容。但有一个巧妙的技巧可以解决这个问题


#![allow(unused)]
fn main() {
let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { panic!("Can't deal with {}, just exit here", error); }
};
println!("file content: {}", content);
}

我们可以在 match 块之后使用 content 中的 String。如果 result 是一个错误,String 将不存在。但由于程序会在它到达使用 content 的地方之前退出,所以没关系。

这可能看起来很激烈,但它非常方便。如果您的程序需要读取该文件,并且如果文件不存在则无法执行任何操作,则退出是一种有效的策略。Result 上甚至有一个快捷方法,称为 unwrap


#![allow(unused)]
fn main() {
let content = std::fs::read_to_string("test.txt").unwrap();
}

无需惊慌

当然,中止程序并不是处理错误的唯一方法。我们可以用 return 替换 panic!

fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { return Err(error.into()); }
};
Ok(())
}

但是,这会改变我们的函数所需的返回类型。事实上,一直以来,我们的示例中都隐藏着一些东西:这段代码所在的函数签名。在最后一个使用 return 的示例中,它变得很重要。以下是完整示例

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result = std::fs::read_to_string("test.txt");
    let content = match result {
        Ok(content) => { content },
        Err(error) => { return Err(error.into()); }
    };
    println!("file content: {}", content);
    Ok(())
}

我们的返回类型是 Result!这就是为什么我们可以在第二个 match 分支中写 return Err(error);。看到底部有一个 Ok(()) 吗?它是函数的默认返回值,表示“结果正常,并且没有内容”。

问号

就像调用 .unwrap() 是使用 panic! 在错误分支中进行 match 的快捷方式一样,我们还有另一个快捷方式用于在错误分支中 returnmatch?

没错,一个问号。您可以将此运算符附加到类型为 Result 的值,Rust 将在内部将其扩展为与我们刚刚编写的 match 非常类似的东西。

试试看

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("test.txt")?;
    println!("file content: {}", content);
    Ok(())
}

非常简洁!

提供上下文

在您的 main 函数中使用 ? 时出现的错误是可以的,但它们并不理想。例如:当您运行 std::fs::read_to_string("test.txt")? 但文件 test.txt 不存在时,您会得到以下输出

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

在您的代码不包含文件名的情况下,很难判断哪个文件是 NotFound。有多种方法可以解决这个问题。

例如,我们可以创建自己的错误类型,然后使用它来构建自定义错误消息

#[derive(Debug)]
struct CustomError(String);

fn main() -> Result<(), CustomError> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .map_err(|err| CustomError(format!("Error reading `{}`: {}", path, err)))?;
    println!("file content: {}", content);
    Ok(())
}

现在,运行它,我们将得到自定义错误消息

Error: CustomError("Error reading `test.txt`: No such file or directory (os error 2)")

不太漂亮,但我们稍后可以轻松地调整我们类型的调试输出。

这种模式实际上非常常见。不过它有一个问题:我们没有存储原始错误,只存储了它的字符串表示。经常使用的 anyhow 库对此有一个巧妙的解决方案:类似于我们的 CustomError 类型,它的 Context 特征可用于添加描述。此外,它还保留了原始错误,因此我们得到了一系列错误消息,指出了根本原因。

首先,通过将 anyhow = "1.0" 添加到我们 Cargo.toml 文件的 [dependencies] 部分来导入 anyhow crate。

完整的示例如下所示

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("could not read file `{}`", path))?;
    println!("file content: {}", content);
    Ok(())
}

这将打印一个错误

Error: could not read file `test.txt`

Caused by:
    No such file or directory (os error 2)

总结

您的代码现在应该看起来像这样

use anyhow::{Context, Result};
use clap::Parser;

/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
    /// The pattern to look for
    pattern: String,
    /// The path to the file to read
    path: std::path::PathBuf,
}

fn main() -> Result<()> {
    let args = Cli::parse();

    let content = std::fs::read_to_string(&args.path)
        .with_context(|| format!("could not read file `{}`", args.path.display()))?;

    for line in content.lines() {
        if line.contains(&args.pattern) {
            println!("{}", line);
        }
    }

    Ok(())
}