更好地反馈错误
我们都知道,错误有时是无可避免的。与其它许多语言不同, 在使用 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
。如果 result
是一个错误,
字符串就不存在。但好在,程序会在我们使用 content
之前就退出了。
这种做法看上去有些极端,却是十分实用的。如果你的程序需要读取一个文件,
且在文件不存在时无法执行任何操作,那么退出是十分合理、有效的选择。
在 Result
中还有一个快捷方法,叫 unwrap
:
#![allow(unused)] fn main() { let content = std::fs::read_to_string("test.txt").unwrap(); }
无须 panic
当然,退出程序并非处理错误的唯一办法。除 !panic
之外,实现 return
也很简单:
fn main() -> Result<(), Box<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()
相当于 match
中快捷设置错误分支中 panic!
,
我们还有另一个快捷的调用 ?
使得在 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(()) }
很简洁是吧!
Providing Context
在 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
特征可以用来添加描述信息。此外,它还能存储下原始错误,
所以我们得到了一个指出根本原因的错误信息链。
在 Cargo.toml
中的 [dependencies]
字段添加上 anyhow = "1.0"
。
一个完整的示例如下:
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)