4.7.错误处理
最后更新于:2022-04-01 00:43:16
> The best-laid plans of mice and men Often go awry
>
> "Tae a Moose", Robert Burns
>
> 不管是人是鼠,即使最如意的安排设计,结局也往往会出其不意
>
> 《致老鼠》,罗伯特·彭斯
有时,杯具就是发生了。有一个计划来应对不可避免会发生的问题是很重要的。Rust提供了丰富的处理你软件中可能(让我们现实点:将会)出现的错误的支持。
主要有两种类型的错误可能出现在你的软件中:失败和恐慌。让我们先看看它们的区别,接着讨论下如何处理他们。再接下来,我们讨论如何将失败升级为恐慌。
## 失败 vs. 恐慌
Rust使用了两个术语来区别这两种形式的错误:失败和恐慌。_失败_(_failure_)是一个可以通过某种方式恢复的错误。_恐慌_(_panic_)是不能够恢复的错误。
“恢复”又是什么意思呢?好吧,大部分情况,一个错误的可能性是可以预料的。例如,考虑一下`from_str`函数:
~~~
from_str("5");
~~~
这个函数获取一个字符串参数然后把它转换为其它类型。不过因为它是一个字符串,你不能够确定这个转换是否能成功。例如,这个应该转坏成什么呢?
~~~
from_str("hello5world");
~~~
这不能工作。所以我们知道这个函数只对一些输入能够正常工作。这是我们期望的行为。我们叫这类错误为_失败_。
另一方面,有时,会出现意料之外的错误,或者我们不能从中恢复。一个典型的例子是`assert!`:
~~~
assert!(x == 5);
~~~
我们用`assert!`声明某值为true。如果它不是true,很糟的事情发生了。严重到我们不能再当前状态下继续执行。另一个例子是使用`unreachable!()`宏:
~~~
enum Event {
NewRelease,
}
fn probability(_: &Event) -> f64 {
// real implementation would be more complex, of course
0.95
}
fn descriptive_probability(event: Event) -> &'static str {
match probability(&event) {
1.00 => "certain",
0.00 => "impossible",
0.00 ... 0.25 => "very unlikely",
0.25 ... 0.50 => "unlikely",
0.50 ... 0.75 => "likely",
0.75 ... 1.00 => "very likely",
}
}
fn main() {
std::io::println(descriptive_probability(NewRelease));
}
~~~
这会给我们一个错误:
~~~
error: non-exhaustive patterns: `_` not covered [E0004]
~~~
虽然我们知道我们覆盖了所有可能的分支,不过Rust不能确定。它不知道概率是在0.0和1.0之间的。所以我们加上另一个分支:
~~~
use Event::NewRelease;
enum Event {
NewRelease,
}
fn probability(_: &Event) -> f64 {
// real implementation would be more complex, of course
0.95
}
fn descriptive_probability(event: Event) -> &'static str {
match probability(&event) {
1.00 => "certain",
0.00 => "impossible",
0.00 ... 0.25 => "very unlikely",
0.25 ... 0.50 => "unlikely",
0.50 ... 0.75 => "likely",
0.75 ... 1.00 => "very likely",
_ => unreachable!()
}
}
fn main() {
println!("{}", descriptive_probability(NewRelease));
}
~~~
我们永远也不应该触发`_`分支,所以我们使用`unreachable!()`宏来表明它。`unreachable!()`返回一个不同于`Result`的错误。Rust叫这类错误为恐慌。
## 使用`Option`和`Result`来处理错误
最简单的表明函数会失败的方法是使用`Option`类型。还记得我们的`from_str()`例子吗?这是它的函数标记:
~~~
pub fn from_str<A: FromStr>(s: &str) -> Option<A>
~~~
`from_str()`返回一个`Option`。如果转换成功了,会返回`Some(value)`,如果失败了,返回`None`。
这对最简单的情况是合适的,不过在出错时并没有给出足够的信息。如果我们想知道“为什么”转换失败了呢?为此,我们可以使用`Result`类型。它看起来像:
~~~
enum Result<T, E> {
Ok(T),
Err(E)
}
~~~
Rust自身提供了这个枚举,所以你不需要在你的代码中定义它。`Ok(T)`变体代表成功,`Err(E)`代表失败。在所有除了最普通的情况都推荐使用`Result`代替`Option`作为返回值。
这是一个使用`Result`的例子:
~~~
#[derive(Debug)]
enum Version { Version1, Version2 }
#[derive(Debug)]
enum ParseError { InvalidHeaderLength, InvalidVersion }
fn parse_version(header: &[u8]) -> Result<Version, ParseError> {
if header.len() < 1 {
return Err(ParseError::InvalidHeaderLength);
}
match header[0] {
1 => Ok(Version::Version1),
2 => Ok(Version::Version2),
_ => Err(ParseError::InvalidVersion)
}
}
let version = parse_version(&[1, 2, 3, 4]);
match version {
Ok(v) => {
println!("working with version: {:?}", v);
}
Err(e) => {
println!("error parsing header: {:?}", e);
}
}
~~~
这个例子使用了个枚举,`ParseError`,来列举各种可能出现的错误。
## `panic!`和不可恢复错误
当一个错误是不可预料的和不可恢复的时候,`panic!`宏会引起一个恐慌。这回使当前线程崩溃,并给出一个错误:
~~~
panic!("boom");
~~~
给出:
~~~
thread '<main>' panicked at 'boom', hello.rs:2
~~~
当你运行它的时候。
因为这种情况相对稀少,保守的使用恐慌。
## 升级失败为恐慌
在特定的情况下,即使一个函数可能失败,我们也想把它当成恐慌。例如,`io::stdin().read_line()`返回一个`IoResult`,一种形式的`Result`(目前只有Result了,坐等文档更新),当读取行出现错误时。这允许我们处理和尽可能从错误中恢复。
如果你不想处理这个错误,或者只是想终止程序,我们可以使用`unwrap()`方法:
~~~
io::stdin().read_line().unwrap();
~~~
如果`Option`是`None`的话`unwrap()`会`panic!`。这基本上就是说“给我一个值,然后如果出错了的话,直接崩溃。”这与匹配错误并尝试恢复相比更不稳定,不过它的处理明显更短小。有时,直接崩溃就行。
这是另一个比`unwrap()`稍微聪明点的做法:
~~~
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
~~~
`ok()`将`Result`转换为`Option`,然后`expect()`做了和`unwrap()`同样的事,不过带有一个信息。这个信息会传递给底层的`panic!`,当错误是这样可以提供更好的错误信息。
## 使用`try!`
当编写调用那些返回`Result`的函数的代码时,错误处理会是烦人的。`try!`宏在调用栈上隐藏了一些衍生错误的样板。
它可以代替这些:
~~~
use std::fs::File;
use std::io;
use std::io::prelude::*;
struct Info {
name: String,
age: i32,
rating: i32,
}
fn write_info(info: &Info) -> io::Result<()> {
let mut file = File::open("my_best_friends.txt").unwrap();
if let Err(e) = writeln!(&mut file, "name: {}", info.name) {
return Err(e)
}
if let Err(e) = writeln!(&mut file, "age: {}", info.age) {
return Err(e)
}
if let Err(e) = writeln!(&mut file, "rating: {}", info.rating) {
return Err(e)
}
return Ok(());
}
~~~
为下面这些代码:
~~~
use std::fs::File;
use std::io;
use std::io::prelude::*;
struct Info {
name: String,
age: i32,
rating: i32,
}
fn write_info(info: &Info) -> io::Result<()> {
let mut file = try!(File::open("my_best_friends.txt"));
try!(writeln!(&mut file, "name: {}", info.name));
try!(writeln!(&mut file, "age: {}", info.age));
try!(writeln!(&mut file, "rating: {}", info.rating));
return Ok(());
}
~~~
在`try!`中封装一个表达式会返回一个未封装的正确(`Ok`)值,除非结果是`Err`,在这种情况下`Err`会从当前函数提早返回。
值得注意的是你只能在一个返回`Result`的函数中使用`try!`,这意味着你不能在`main()`中使用`try!`,因为`main()`不返回任何东西。
`try!`使用[From](http://doc.rust-lang.org/nightly/std/convert/trait.From.html)特性来确定错误时应该返回什么。