Rust错误处理详解
错误处理是应用程序应对异常的能力。应用程序可以采取主动或被动的方式进行错误处理。
- 主动错误处理 Rust提供了Result和Option这两个标准类型
- 被动错误处理 Rust提供了panic。当执行过程中(运行时)发生了无法继续运行的异常事件时,就会引发panic。
Rust 彻底放弃了“异常”这种隐式传播的思路,而是走编译期强制处理错误的路线
-
可恢复错误(Recoverable Errors):
用 Result<T, E> 类型表示。调用者必须显式处理(match、? 运算符)。
-> 这就是所谓的主动错误处理,错误传播路径在类型系统里清清楚楚。 -
不可恢复错误(Unrecoverable Errors):
用 panic! 表示。通常意味着程序逻辑出现严重问题,无法也不应该继续执行。
-> 这就是 Rust 的被动错误处理,类似“终止程序”的紧急刹车。
panic 和异常的区别
虽然 panic 和异常都基于栈展开(stack unwinding),但理念不同:
- 异常:设计成可以恢复的一般错误机制。
- panic:设计成“程序出 bug 或进入不可能状态时”的终止信号。
fn divide_two_numbersing_ratio(first: i32, second: i32)-> i32 {first/second
}fn logic() {divide_two_numbersing_ratio(1, 0);
}
fn main() {logic();
}
当panic发生时,栈会按顺序展开: divide_two_numbersing_ratio和logic以及最后的main函数。
在程序终止时会输出用于诊断的错误信息,其中包含了panic发生的具体位置
thread 'main' panicked at src/bin/13_error.rs:2:4:
attempt to divide by zero
错误信息中还包含了调用栈回溯信息。
stack backtrace:0: __rustc::rust_begin_unwindat /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/panicking.rs:697:51: core::panicking::panic_fmtat /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/panicking.rs:75:142: core::panicking::panic_const::panic_const_div_by_zeroat /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/panicking.rs:175:173: _13_error::divide_two_numbersing_ratioat ./src/bin/13_error.rs:2:44: _13_error::logicat ./src/bin/13_error.rs:6:55: _13_error::mainat ./src/bin/13_error.rs:9:56: core::ops::function::FnOnce::call_onceat /home/cangli/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:253:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
最后提示你可以运行 RUST_BACKTRACE=full cargo run
,获取更详细的调用堆栈信息。
当发生panic时,栈展开的过程为应用程序提供了有序退出时机,这时最重要的是释放占用的资源和内存。
- 有些清理工作是自动完成的。比如删除局部变量
- 有些需要特殊处理。比如堆内存对外部资源的引用。
在栈展开的过程中,实现了Drop trait的值会自动调用drop函数。相当于其他语言的析构函数。
struct Person {name: String,age: i32,
}impl Drop for Person {fn drop(&mut self) {println!("drop被调用");}
}fn division(first: i32, second: i32) {let p = Person{name: "李四".to_string(), age: 18};first / second;
}fn logic() {let p = Person{name: "张三".to_string(), age: 18};division(1, 0);
}
fn main() {logic();
}
如果你没有想好为panic事件准备好清理策略,这种情况下,栈展开的过程可能会失去作用。更重要的是,如果没有合适的清理措施,应用程序可能会陷入未知或不稳定的状态,这种情况下最佳方案就是在panic发生时直接终止应用程序。
在Cargo.toml中添加配置来实现这一行为
[profile.dev]
panic = "abort"
需要注意的是,任何人都可以改变这个外部配置,也就意味着这种行为可能会在你的控制之外发生。除了栈回溯外,这也是panic不可预测的并且应该避免的另一种原因。
panic!宏
panic是由异常情况引发的。其实也可以使用panic!宏来主动强制引发panic.
如果可以的话还是要尽量避免panic。panic!的场景
- 传播现有的panic
- 无法找到可行的解决方案
- 向应用程序发出无法拒绝的通知
fn main() {panic!("hello world")
}
包含了自定义描述信息
thread 'main' panicked at src/bin/13_error.rs:2:5:
hello world
Rust还提供一种高级版本的panic!宏,支持格式化字符串。
fn main() {panic!("hello {}", "张三")
}
处理panic
你可以根据实际情况采取不同措施。
避免栈展开进入外部代码,这可能导致无法预知的行为。比如展开到系统调用就可能引发各种问题。
在Rust中最好做必要的错误处理,如果实在无法进行错误处理,那么可以对panic进行有限的处理,比如记录panic信息并重新触发panic
- catch_unwind方法用于处理panic,在std::panic模块中,
fn catch_unwind<F: FnOnce()->R+UnwindSafe,R>(f: F)->Result<R>
catch_unwind接受一个闭包作为参数,并返回一个Result
- 如果闭包没有触发panic,则返回Ok(value) (其中value是闭包调用的结果)
- 当发生panic时,该函数返回Err(error),其中error是panic的错误值
use std::any::Any;
use std::panic::catch_unwind;fn get_data(request: usize)->Result<i8, Box<dyn Any+Send>> {let vec:Vec<i8> = (0..100).collect();let ret = catch_unwind(||vec[request]);ret}
fn main() {match get_data(100) {Ok(value) => println!("{}",value),Err(_) => println!("数组访问异常")}}
可以看到,即使进行了处理,panic消息依然被输出。
其实每个线程都有一个panic hook,它是一个在panic发生时会被调用并输出panic的函数。正是这个hook函数将上面的回溯信息输出出来,当这个功能被启用的情况下,使用std::panic模块中的set_hook函数来替换这个hook,panic hook的调用在panic发生和处理之间。
解决办法是使用一个空的闭包替换默认的hook来去除panic信息
use std::any::Any;
use std::panic;fn get_data(request: usize)->Result<i8, Box<dyn Any+Send>> {let vec:Vec<i8> = (0..100).collect();panic::set_hook(Box::new(|_info|{}));let ret = panic::catch_unwind(||vec[request]);ret}
fn main() {match get_data(100) {Ok(value) => println!("{}",value),Err(_) => println!("数组访问异常")}}
要能够处理panic首先需要了解panic。与panic相关的信息会以任何类型提供,需要先将其转换为特定类型,然后才能访问有关panic的具体信息。
在前面代码中,我们忽略了panic的具体信息,现在我们需要输出panic的信息。将其向下转型为String
use std::any::Any;
use std::panic;fn get_data(request: usize)->Result<i8, Box<dyn Any+Send>> {let vec:Vec<i8> = (0..100).collect();panic::set_hook(Box::new(|_info|{}));let ret = panic::catch_unwind(||vec[request]);ret}
fn main() {match get_data(100) {Ok(value) => println!("{}",value),Err(error) => println!("{:?}",error.downcast::<String>()),}}
unwrap
在应用程序开发和测试阶段,许多开发者会使用unwrap函数来简化错误处理。
通过unwrap可以把Result或Option中的错误结果转换为panic,这种做法有两个原因。
- 在开发阶段,展示没想好如何处理这些特定的错误。
- 想确保错误不会被忽略。
不过unwrap函数有一些变体适用于更多的场景,而不仅仅是开发阶段。
首先介绍unwrap函数
如果Option/Result的值为None/Err(E),就会引发panic
fn main() {let vec = [1,2,3,5,5];let ret = vec.get(5).unwrap();println!("{}",ret);
}
expect
可以用expect替换unwrap函数,区别是expect可以在panic时指定错误信息,而unwrap只有默认信息。
fn main() {let vec = [1,2,3,5,5];let ret = vec.get(5).expect("索引越界");println!("{}",ret);
}
有些unwrap函数的变体出现错误时不会引发panic,这样的方法对于错误处理非常有用。甚至可以处于非开发阶段。
unwrap_or
当出现错误时,这个方法会返回一个预设的替代值而不是直接引发panic。
fn main() {let vec = [1,2,3,5,5];let ret = vec.get(5).unwrap_or(&100);println!("{}",ret);
}
另外一个变体是unwrap_or_else
unwrap_or_else
替代值是一个闭包,这个替代值需要通过计算或涉及复杂逻辑时很有用。当unwrap出现错误(None/Err)时,会自动调用这个闭包。
fn main() {let vec = [1,2,3,5,5];let ret = vec.get(5).unwrap_or_else(|| &100);println!("{}",ret);
}
unwrap_or_default
在遇到错误时会返回对应类型的默认值。返回的具体默认值由类型决定,比如整型的默认值是0。需要注意的是,并非所有类型都有默认值,只要实现了Default trait的类型才有相应的默认值,而引用没有实现这个trait,所以不能使用这个方法。
fn main() {let vec: Option<i8> = None;let ret = vec.unwrap_or_default();println!("{}",ret);
}