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)特性来确定错误时应该返回什么。
';