Rust将错误分为两大类:可恢复错误与不可恢复错误。其他大部分变成语言都没有可以区分这两种错误,而是通过异常之类的机制来统一处理它们。虽然Rust没有类似的异常处理机制,但它提供了用于可恢复错误的类型Result<T,F>
,以及在程序出现不可恢复错误时中止运行的panic!
宏。
一、不可恢复错误与panic!
程序会在panic!
宏执行时打印出一段错误提示信息,展开并清理当前的调用栈,然后退出程序。
panic中的栈展开与终止
1)当panic发生时,程序会默认开始栈展开。这意味着Rust会沿着调用栈的反向顺序遍历所有调用函数,并依次清理这些函数中的数据。
2)当panic发生时我们还可以立即终止程序,它会直接结束程序且不进行任何清理工作,程序所使用过的内存只能由操作系统进行回收。
调用panic!
:
fn main() {
panic!("crash and burn");
}
由于调用了panic!
,因此输出了最后两行错误提示信息。第一行显示了我们向panic
所提供的信息,并指出了源代码中panic
所发生的位置:src\main.rs:2:5
表明panic
发生在文件src\main.rs
中的第二行的第五个字符处。
1、使用panic!
产生回溯信息
下面代码,它没有直接在代码中调用panic!
宏,但会因为其中代码的bug而导致标注库中产生panic!
。
fn main() {
let v = vec![1, 2, 3];
v[99];
}
这段代码中,动态函数只有持有3个元素,但我们却在尝试访问它的第100个元素。在这种情况下,Rust会触发panic。
在类似于C语言中,程序在这种情况下依然会尝试访问你所请求的值,即便这可能会于你所期望的并不相符;你会得到动态数组中对应这个索引位置的内存,而这个内存可能存储了其他数据,甚至都不属于动态数组本身,这种情形也被称为缓冲区溢出,并可能导致严重的安全性问题。
我们可通过RUST_BACKTRACE=1
得到回溯信息,进而确定触发错误的原因。回溯(backtrace)中包含了到达错误点的所有调用函数列表。
二、可恢复错误与Result
Result
类型来处理可能失败的情况,该枚举定义了两个变体——Ok和Err,它们都是泛型参数。
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
T
代表了Ok变体中包含的值类型,该变体中的值会在执行成功时返回;而E
则代表了Err变体中包含的错误类型,该变体中的值会在执行失败时返回。
调用一个运行失败的函数,它会返回Result值作为运行结果。
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
}
如何知道相关函数是否会返回Result?
- 翻阅标准库API文档;std - Rust (rust-lang.org)
- 直接向编译器索要答案;
use std::fs::File;
fn main() {
let f: u32 = File::open("hello.txt");
}
上述输出表明,File::open
函数的返回类型是Result<T, E>
。这里的泛型参数T被替换为了成功值得类型File
,也就是文件的句柄,而错误值所对应的类型E则被替换为了std::io::Error
。
当File::open
函数运行成功时,变量f
中的值将会是一个包含文件句柄的Ok实例。当它运行失败时,变量f
中的值则会使一个包含了用于描述错误种类信息的Err实例。
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("There was a problem opening the file {:?}", error)
},
};
}
注意,与Option
枚举一样,Result枚举及其变体已经通过与导入模块被自动地引入当前作用域中,所以我们不需要在使用Ok变体与Err变体之前在match
分支中显示声明Result::
。
- 当Result的结果是Ok的时候,将Ok变体内部的file值移出,并将这个文件句柄重新绑定至变量
f
。 - 另一个分支则处理了
File::open
返回Err值得情况。我们选择通过调用panic!
宏来处理该情形。
1、匹配不同的错误
当文件不存在时,创建该文件;其他错误继续返回。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Tried to create file but there was a problem: {:?}", e)
},
other_error => panic!("There was a problem opening the file: {:?}", other_error),
},
};
}
File::open
返回的Err变体中的错误值类型,是定义在某个标准库中的结构体类型:io::Error
。这个结构体拥有一个被称作kind的方法,可通过它来获得io::ErrorKind
值,这个io::ErrorKind
枚举是由标注库提供的,它的变体被用于描述io操作所可能导致的不同错误。其中ErrorKind::NotFound
,它用于说明我们尝试打开的文件不存在。
在这个匹配分支中,我们检查error.kind()
返回的值是不是ErrorKind枚举的NotFound变体;如果是,我们就接着使用函数File:create
来创建这个文件。
然而,由于File::create
本身也可能运行事变,所以我们也需要对它的返回值条件一个match表达式。外部match的最后一个分支保持不变,用于在出现其余错误时让程序除法panic。
如下代码与上述代码拥有完全一致的性,但没有相对复杂的match表达式,更为清晰易读:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt").map_err(|error| {
if error.kind() == ErrorKind::NotFound {
File::create ("hello.txt").unwrap_or_else(|error| {
panic!("Tried to create file but there was a problem: {:?}", error);
})
} else {
panic!("There was a problem opening the file: {:?}", error);
}
});
}
2、失败时除法panic的快捷方式:unwrap和expect
类型Result<T, E>
自身定义了许多辅助方法来应对各种任务。其中一个被称为unwrap的方法实现了我们在上述实例中match表达式的效果。当Result的返回值是Ok变体时,unwrap
则会替我们调用panic!
宏。
fn main() {
let f = File::open("hello.txt").unwrap();
}
还有另外一个被称作expect
的方法,它允许我们在unwrap的基础上指定panic!
所附带的错误提示信息。使用expect
并附带上一段清晰的错误提示信息可以阐明的意图,并使你更容易追踪到panic
的起源。
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
3、传播错误
当你编写的函数中包含了一些可能会执行失败的调用时,除了可以在函数中处理这个错误,还可以将这个错误返回给调用者,让他们决定应该如何做进一步处理,这个过程被称作传播错误(progagating)。
如下代码9-6,展示了一个从文件中读取用户名的函数。当文件不存在或无法读取时,这个函数会将错误作为作为结果返回给自己的调用者:
fn main() {
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
}
传播错误模式在Rust编程中非常常见,所以Rust抓们提供了一个问号运算符(?
)来简化它的语法。
1.传播错误的快捷方式:?
运算符
通过将?
放置于Result值之后,我们将实现与上述实例使用match表达式来处理Result时一样的功能。
fn main() {
use std::io;
use std::io::Read;
use std::fo::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
}
不过,在使用match
表达式与?
还是在一个区别:被*?
运算符所接受的错误值会隐式被from
函数处理*,这个函数定义于标准库的From trait
中,用于在错误类型之间进行转换。当?
运算符调用from
函数时,它就开始尝试将传入的错误类型转换为当前函数的返回错误类型。当一个函数拥有不同的失败原因,却使用了统一的错误返回类型来同时进行表达式,这个功能会十分有用。只要每个错误类型都实现了转换为返回错误类型的from
函数,?
运算符就会自动帮我们处理所有的转换过程。
?
运算符帮忙我们消除了大量模板代码,使函数更为简单。还可通过链式方法调用来进一步简化代码:
fn main() {
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
}
创建新String
并赋值给s的语句移动到了函数开始的地方,这一部分没有任何改变。接下来,我们并没有创建变量f
,而是直接将read_to_string
链接至File::open("hello.txt")?
所产生的结果处并进行调用。我们依然依然在read_to_string
调用的尾部保留了?
,并依然会在File::open
和read_to_string
都运行成功时,返回一个包含了用户名s
的Ok值。这种写法跟符合项目实践的写法。
2.?
运算符只能被用于返回Result的函数
使用了?
运算符的函数必须返回Result、Option或任何实现了std::ops::Try
的类型。在那些没有返回上述类型的函数里,一旦调用的其他函数返回了Result<T, E>
,就需要使用match
或Result<T, E>
自身的方法来对Result<T, E>
进行恰当的处理。
三、要不要使用panic!
只要你认为自己可以代替调用者决定某种情形是不可恢复的,那么就可以使用panic!
;当你选择返回一个Result
值时,你就将这种选择权交给了调用者。