错误处理
在Rust中,错误被分成两大类:可恢复错误与不可恢复错误。Rust没有异常机制,而是为这两种错误分别提供了一些方法
- 可恢复错误:需要将错误报告给用户并再次进行尝试。使用
Result<T,E>
- 不可恢复错误:bug。使用
panic!
宏
不可恢复错误与panic!
程序会在panic!宏执行时打印出一段错误提示信息,展开并清理当前调用栈,退出程序
fn main() {
panic!("crash!");
}
运行程序,会显示错误信息
PS E:\Rust\Demo\panic> cargo run
Compiling panic v0.1.0 (E:\Rust\Demo\panic)
Finished dev [unoptimized + debuginfo] target(s) in 1.68s
Running `target\debug\panic.exe`
thread 'main' panicked at 'crash!', src\main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
关注最后两行,第一行显式我们向panic!提供的信息,并指出源代码panic发生的位置
使用panic!产生的回溯信息
在上面错误信息的最后一行"note: run with RUST_BACKTRACE=1
environment variable to display a backtrace"
意思是我们通过修改环境变量RUST_BACKTRACE=1
,来得到回溯信息
回溯中包含了到达错误点的所有调用函数列表
例如我们的程序是
fn main() {
let v = vec![1,2,3];
v[100];
}
不启用回溯的错误信息:
PS E:\Rust\Demo\panic> cargo run
Compiling panic v0.1.0 (E:\Rust\Demo\panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target\debug\panic.exe`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', src\main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
在run之前加入RUST_BACKTRACE=1
启用回溯后的错误信息
(注意,用windows系统需要$env:RUST_BACKTRACE=1 ; cargo run
)
PS E:\Rust\Demo\panic> $env:RUST_BACKTRACE=1 ; cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\panic.exe`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', src\main.rs:3:5
stack backtrace:
0: std::panicking::begin_panic_handler
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library\std\src\panicking.rs:579
1: core::panicking::panic_fmt
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library\core\src\panicking.rs:64
2: core::panicking::panic_bounds_check
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library\core\src\panicking.rs:159
3: core::slice::index::impl$2::index<i32>
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\library\core\src\slice\index.rs:260
4: core::slice::index::impl$0::index
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\library\core\src\slice\index.rs:18
5: alloc::vec::impl$13::index<i32,usize,alloc::alloc::Global>
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\library\alloc\src\vec\mod.rs:2703
6: panic::main
at .\src\main.rs:3
7: core::ops::function::FnOnce::call_once<void (*)(),tuple$<> >
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\library\core\src\ops\function.rs:250
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
可恢复错误与Result
Result是一个枚举类型
enum Result<T,E> {
Ok(T),
Err(E),
}
执行成功时,返回Ok变体;执行失败时,返回Err变体
同Option<T>
一样,使用match进行处理
use std::fs::File;
fn main() {
let f = File::open("hello.tex");
let f = match f{
Ok(file) => file,
Err(error) => {
panic!("{:?}",error);
},
}
}
在这个特定的例子中,泛型参数T
是std::fs::File
是文件的句柄;E
是std::io::Error
匹配不同的错误
使用嵌套match配合方法error.kind()
来匹配不同的错误
use std::fs::File;
fn main() {
let f = File::open("hello.tex");
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!("creating file but a problem:{:?}",e),
},
other_error => panic!("opening the file with problem:{:?}",other_error),
},
};
}
如果错误类型是枚举量io::ErrorKind::NotFound
,就尝试创建一个新文件。注意创建时又有一个match来处理新建文件可能出现的问题
失败时触发panic的快捷方式:unwrap和expect
这两个方法相当于打包了我们上面的match逻辑
如果Result是Ok变体,这两个方法就会返回Ok变体里附加的值;如果是Err变体,这两个方法会触发panic。不同点在于,触发panic时,unwrap只会打印一段简短的默认信息;而expect(…)则允许我们自定义一段附加信息
fn main() {
let f = File::open("hello.tex").unwrap();
}
fn main() {
let f = File::open("hello.tex").expect("Failed to open hello.tex");
}
传播错误
像是在其它编程语言我们不停的向上层抛出异常一样,在Rust中向函数的调用者传播错误也是一种常见的做法
最简单的,我们将函数设计为返回Result类型。并将Result类型设计为第一个参数类型是函数执行成功时应该返回的类型;第二个参数类型是函数在运行时可能出现的错误类型
use std::fs::File;
use std::io::{self,Read};
fn read_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),
}
}
传播错误的快捷方式:?运算符
将?放于Result值之后,如果值是Ok,包含在Ok中的值就会作为表达式的结果返回并继续执行函数;如果是Err,那么这个值就会作为整个函数的结果返回
可以使用?运算符改编上一个程序,并保持功能基本上不变
唯一与match版本的区别是:被?运算符接收的错误值会隐式的被from函数处理,该函数总是尝试将?运算符传入的错误类型转化为函数返回值的错误类型
use std::fs::File;
use std::io::{self,Read};
fn read_from_file() -> Result<String,io:Error> {
let f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
甚至采用链式调用来进一步缩短程序
use std::fs::File;
use std::io::{self,Read};
fn read_from_file() -> Result<String,io:Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
?运算符只能用于返回Result的函数
比如我们的一般main函数返回值是(),自然不能使用?运算符
那有没有方法在main函数中使用?
有,那就是变main函数的返回类型
use std::error::Error;
use std::fs::File;
fn main() -> Result<(),Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(());
}
要不要使用panic!
示例、原型和测试
使用panic
- 编写示例时,为了健壮性添加错误处理代码往往会减弱示例的可读性。此时应该直接使用unwrap之类可能导致panic的方法作为某种占位符,用来标明这些是需要程序进一步处理的错误
- 在原型中,不确定具体错误的具体处理方式,一般也是使用unwrap和expect来占位
- 在测试中,快速判断测试是否失败
当你比编译器拥有更多信息时
当你拥有某些逻辑可以确保Result一定是一个Ok值时,直接使用unwrap来简化代码
比如
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap();
错误处理的指导原则
损坏状态:
- 设计中的一些假设、保证、约定或不可变性出现了被打破的情形
- 损坏状态不包括预期中偶尔会发生的事情
- 代码无法在出现损坏状态后继续正常运行
- 没有合适方法将“处于损坏状态”这一信息,编码至我们所使用的类型中
几条原则:
- 出现损坏时,最好是触发panic来终止程序
- 错误是可预期的,使用Result
- 代码基于某些值进行操作时,首先验证值的有效性,并在无效时触发panic
创建自定义类型来进行有效性验证
Rust的类型系统会自动的做有效性验证,比如你无法给u32赋值一个负数
当我们创建自己的新类型时,如何保证有效性呢?
一个做法是 setter-getter:setter为值进行设置,并检查值是否有效;getter来进行取值;注意值要设置为私有的,保证不能有除setter之外的手段来修改值
一个例子:值必须在1~100的整数类型
pub struct Num {
value:i32,
}
impl Num {
pub fn new(value:i32) -> Num {
if value < 1 || value > 100 {
panic!("illigal value {} for Num!",value);
}
Num {
value
}
}
pub fn value(&self) -> i32 {
self.value;
}
}
其中setter是关联函数new,该函数检查值是否合法,不合法时触发panic
getter是方法value
字段value设置为私有