> 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)特性来确定错误时应该返回什么。
- 前言
- 1.介绍
- 2.准备
- 2.1.安装Rust
- 2.2.Hello, world!
- 2.3.Hello, Cargo!
- 3.学习Rust
- 3.1.猜猜看
- 3.2.哲学家就餐问题
- 3.3.其它语言中的Rust
- 4.高效Rust
- 4.1.栈和堆
- 4.2.测试
- 4.3.条件编译
- 4.4.文档
- 4.5.迭代器
- 4.6.并发
- 4.7.错误处理
- 4.8.外部语言接口
- 4.9.Borrow 和 AsRef
- 4.10.发布途径
- 5.语法和语义
- 5.1.变量绑定
- 5.2.函数
- 5.3.原生类型
- 5.4.注释
- 5.5.If语句
- 5.6.for循环
- 5.7.while循环
- 5.8.所有权
- 5.9.引用和借用
- 5.10.生命周期
- 5.11.可变性
- 5.12.结构体
- 5.13.枚举
- 5.14.匹配
- 5.15.模式
- 5.16.方法语法
- 5.17.Vectors
- 5.18.字符串
- 5.19.泛型
- 5.20.Traits
- 5.21.Drop
- 5.22.if let
- 5.23.trait对象
- 5.24.闭包
- 5.25.通用函数调用语法
- 5.26.包装箱和模块
- 5.27.`const`和`static`
- 5.28.属性
- 5.29.`type`别名
- 5.30.类型转换
- 5.31.关联类型
- 5.32.不定长类型
- 5.33.运算符和重载
- 5.34.`Deref`强制多态
- 5.35.宏
- 5.36.裸指针
- 6.Rust开发版
- 6.1.编译器插件
- 6.2.内联汇编
- 6.3.不使用标准库
- 6.4.固有功能
- 6.5.语言项
- 6.6.链接参数
- 6.7.基准测试
- 6.8.装箱语法和模式
- 6.9.切片模式
- 6.10.关联常量
- 7.词汇表
- 8.学院派研究
- 勘误