更友好的错误报告
我们只能接受错误会发生的事实。与许多其他语言相比,在使用 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 的快捷方式一样,我们还有另一个快捷方式用于在错误分支中 return 的 match:?。
没错,一个问号。您可以将此运算符附加到类型为 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(())
}