更友好的错误报告
我们只能接受错误会发生的事实。与许多其他语言相比,在使用 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(())
}