错误处理
Rust 错误处理
Rust 的可靠性:错误处理。
- 大部分情况下:在编译时提示错误,并处理。
错误的分类:
- 可恢复:例如文件未找到,可以再次尝试。
- 不可恢复:也就是 bug,例如访问的索引超出范围。
Rust 没有类似异常的机制:
- 针对可恢复的错误:
Result<T,E>
。 - 针对不可恢复的错误:panic! 宏。
- 针对可恢复的错误:
不可恢复的错误
当 panic! 宏执行时:
- 程序会打印一个错误信息。
- 展开(unwind)、清理调用栈(Stack)。
- 退出程序。
为了应对 panic,展开或中止(abort)调用栈:
- 默认情况下,当 panic 发生,程序会展开调用栈 (工作量大):
- Rust 沿着调用栈往回走。
- 清理每个遇到的函数中的数据。
- 也可以立即中止调用栈:
- 不进行清理,直接停止程序。
- 内存需要 OS 进行清理。
- 想让二进制文件更小,把设置从”展开“改为”中止“:
- 在 Cargo.toml 中适当的 profile 部分设置:
- panic = ‘abort’
- 在 Cargo.toml 中适当的 profile 部分设置:
- 默认情况下,当 panic 发生,程序会展开调用栈 (工作量大):
使用 panic! 产生的回溯信息
- panic! 可能出现在:
- 我们写的代码中。
- 我们所依赖的代码中。
- 可通过调用 panic! 的函数回溯信息来定位引起问题的代码。
- 通过设置环境变量
RUST_BACKTRACE=1
可得到回溯信息。 - 为了获取带有调试信息的回溯,必须启用调试符号(不带 –release)。
Result 与可恢复的错误
Result 枚举
Result 枚举格式。
- T:操作成功情况下,Ok 变体里返回的数据的类型。
- E:操作失败情况下,Err 变体里返回的错误的类型。
enum Result<T, E> { Ok(T), Err(E), }
处理 Result 的一种方式:match 表达式。
- 和 Option 枚举一样,Result 及其变体也是由 prelude 带入作用域。
let f = File::open("hello.txt"); let file = match f { Ok(file) => file, Err(error) => { panic!("Error open file {:?}", error); } };
匹配不同的错误。
use std::fs::File; use std::io::ErrorKind; fn main() { let f = File::open("hello.txt"); let _file = match f { Ok(file) => file, Err(error) => match error.kind() { ErrorKind::NotFound => match File::create("hello.txt") { Ok(fc) => fc, Err(e) => panic!("Error creating file: {:?}", e), }, other_error => panic!("Error opening the file : {:?}", other_error), }, }; }
闭包(closure)。
Result<T, E>
有很多方法:- 它们接收闭包作为参数。
- 使用 match 实现。
use std::fs::File; use std::io::ErrorKind; let _f = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Error creating file : {:?}", error); }) } else { panic!("Error opening file : {:?}", error); } });
unwrap: match 表达式的一个快捷方法:
let _f = File::open("hello.txt").unwrap();
,这与 match 是类似的:- 当 OK 时 unwrap 会自动返回 OK 的结果。
- 当 Err 时 unwrap 则会自动调用 panic!。
expect: 与 unwrap 功能类似,但是可以指定错误信息:
let _f = File::open("hello.txt").expect("无法打开文件");
传播错误
一般情况都是在函数中处理错误,但有时需要调用者来决定如何处理,这时就需要向上传播错误。
fn read_username_from_file() -> Result<String, std::io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), // return Err 其类型就是 io::Error 枚举类型 }; let mut s = String::new(); match f.read_to_string(&mut s) { // 返回最终结果或错误 Ok(_) => Ok(s), Err(e) => Err(e), } } fn main() { let name = read_username_from_file(); println!("name is {}", name.unwrap()); }
?
运算符:传播错误的一种快捷方式。- 如果 Result 是 Ok:Ok 中的值就是表达式的结果,然后继续执行程序。
- 如果 Result 是 Err:Err 就是
整个函数
的返回值,就像使用了return
。
fn read_username_from_file_by_special() -> Result<String, std::io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) // 最后需要 Ok 是由于前边的 ? 会保证 Err 时有返回,不会保证成功时返回,所以要显式返回下 }
?
与 from 函数:Trait std::convert::From 上的 from 函数主要用于错误之间的转换。- 被
?
所应用的错误,会隐式的被 from 函数处理。当?
调用 from 函数时:- 它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型。
- 主要用于针对不同错误原因,返回同一种错误类型。
- 需要每个错误类型实现了转为所返回的错误类型的 from 函数。
// 链式调用 fn read_username_from_file_by_special2() -> Result<String, std::io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) }
- 被
?
运算符只能返回用于 Result 的函数。/* error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:5:36 | 4 | / fn main() { 5 | | let f = File::open("hello.txt")?; | | ^ cannot use the `?` operator in a function that returns ` ()` 6 | | } | |_- this function should return `Result` or `Option` to accept `?` | = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()` */ fn main() { let f = File::open("hello.txt")?; }
- 优化后,
Box<dyn Error>
是 trait 对象:
fn main() -> Result<(), Box<dyn std::error::Error>> { let f = File::open("hello.txt")?; Ok(()) }
- 优化后,
何时进行 panic
在定义一个可能失败的函数时,优先考虑返回 Result。否则当判断无法进行恢复时则进行 panic。
在编写示例、原型代码、测试时可以分别使用 panic。
错误处理的指导性建议:
- 当代码最终可能处于损坏状态时,最好使用 panic!。
- 损坏状态(Bad state):某些假设、保证、约定或不可变性被打破:
- 例如传入非法值、矛盾的值或空缺的值。
- 这种损坏状态并不是预期能够知道是偶尔发生的事情。
- 在此之后,如果代码处于这种损坏就无法再运行。
- 使用的类型中没有一个好的方法来处理这些信息(损坏状态)。
使用自定义类型来进行检查:
pub struct Guess { value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if !(1..=100).contains(&value) { panic!("Guess value must be between 1 and 100, got {}", value); } Guess { value } } pub fn value(&self) -> i32 { self.value } }