04-更友好的错误报告
4 分钟阅读
更友好的错误报告
我们只能接受一个事实:错误总会发生。 与许多其它语言不同, 在使用 Rust 时很难忽视并回避这一点, 因为它没有异常。 所有可能的错误状态,常常编码在函数的返回类型里。
Results
像 read_to_string 这样的函数并不直接返回字符串。
相反,它返回一个 Result,
其中要么是
一个 String,
要么是某种类型的错误。
此处是 std::io::Error。
怎么知道是哪一种?
因为 Result 是一个 enum,
可以用 match 检查它是哪个变体:
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 各臂必须返回同一类型是个挑战,
但有个巧妙的办法绕过:
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 的快捷方法:
let content = std::fs::read_to_string("test.txt").unwrap();
不必 panic
当然,中止程序不是处理错误的唯一方式。
与其用 panic!,我们可以直接 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()); }
};
# 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(()) 了吗?
它是函数的默认返回值,意思是:
“Result 正常,且没有内容”。
问号运算符
就像调用 .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 不存在时,
会得到这样的输出:
| |
若代码里实际上并没有包含文件名,
就很难判断是哪个文件 NotFound。
有多种处理方式。
其一,可以创建自己的错误类型, 并用它构造自定义错误消息:
| |
运行后会得到自定义错误消息:
| |
不太好看, 但之后可以为该类型调整调试输出。
这种模式非常常见。
不过有一个问题:
我们没有保存原始错误,
只保存了它的字符串表示。
流行的 anyhow 库对此有个漂亮的解法:
它的 Context trait 可以用来添加类似我们 CustomError 类型的描述。
此外,它还会保留原始错误,
于是我们得到一条指向根因的“错误消息链”。
先导入 anyhow crate:在 Cargo.toml 的 [dependencies] 段加入
anyhow = "1.0"。
完整示例如下:
| |
这会打印错误:
| |
收尾
你的代码现在应如下所示:
| |