4.7.错误处理

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叫这类错误为恐慌。

使用OptionResult来处理错误

最简单的表明函数会失败的方法是使用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();

如果OptionNone的话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特性来确定错误时应该返回什么。

文章导航