更好的错误报告
错误总会发生这个事实我们只能选择接受。与其他语言相比,在使用 Rust 时很难不注意和面对这个现实:因为它没有异常,所有可能的错误状态通常都编码在函数的返回类型中。
Results
像 read_to_string
这样的函数并不返回一个字符串,而是返回一个包含 String
或某种类型的错误(这种情况下为 std::io::Error
)的 Result
。
怎么能知道它是什么呢?因为 Result
是个枚举
类型,所以可以使用 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); } } }
Unwrapping
现在,我们可以访问文件中的内容,但是在 match
块之后,我们实际上就不能做任何事了(译者注:因为上面代码中 match 到的类型并未在 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
为 error,这个 content
字符串就不存在。但因为程序会在使用 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(()) }
(译者注:以上这种不在自身内部进行错误处理,而将其包装为 Result 往函数调用者处传递,让函数调用者进行正确值或错误处理的写法被称为传播错误,当然,如果你学过 Java 这类带有异常的语言,就会觉得有点像 throw Exception ,然而两者还是有本质上的区别的。)
然而这改变了我们函数所需的返回类型。实际上,在我们的示例中一直隐藏着一些事物:代码所在函数的签名。上一个示例中的 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
!这就是为什么我们可以在第二个匹配分支写 return Err(error);
的原因。底部为何有 Ok(())
呢?这是函数的默认返回值,意味着 “结果没问题,且没有内容”。
问号
就像调用 .unwrap()
是 match
的 error 分支 panic!
的简便写法一样。我们有另一个 match
中 error 分支 return
的简写:?。
是的,就是一个问号。你可以将该操作符附加到 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(()) }
可以说是非常简洁了!
提供 context
当在 main
函数中使用 ?
获取 error 是没问题的,但是这还不够好。例如:当你运行 std::fs::read_to_string("test.txt")?
,但是 test.txt
并不存在,你会得到这个输出:
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
如果代码中没有字面上包含文件名(译者注:即读取文件函数的参数是一个变量而非字面量),那么很难确定是哪个文件 NotFound
。这里有多种解决办法。
例如,我们可以创建自己的 error 类型,然后用它来构建自定义错误消息:
#[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)")
不是很完美,但是稍后我们可以很容易地根据我们的类型调整 debug 输出。
这种模式实际上很常见。不过它有个问题:我们并不储存原始 error ,仅储存其字符串表示。常用的 anyhow
库有个巧妙的解决方案:与我们的 CustomError
类型类似,它的 Context
trait 可以用来添加描述。除此之外,它还保留了 原始 error ,因此我们会得到指向错误根源的 error “链”。
让我们通过在 Cargo.toml
文件的 [dependencies]
块中添加 anyhow = "1.0"
来首次引入 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)