在软件中出现错误是一件很正常的事,Rust有许多处理错误的方式,大部分情况下,Rust要求用户在编译前判断出现错误的可能并给出相应的举措。这项要求使得你的程序更加健壮,在部署到生产环境前可以发现错误并得到恰当的处理。
Rust将错误分为两类:
- 可恢复错误:例如文件未找到,大部分对应措施是将问题报告给用户或者重试。
- 不可恢复错误:例如硬件不可用,对应的措施是退出程序的运行。
其他编程语言一般不区分这两种情况,使用相同的对应措施,一般这种措施称之为异常处理机制。Rust没有这种机制,它使用Result<T,E>处理可恢复错误,使用panic!宏处理不可恢复错误。
11.1 不可以恢复的错误使用panic!宏
panic!用于处理不可恢复的错误。当程序遇到无法或不应该继续执行的错误时,会触发panic,导致程序终止执行。
- panic的使用场景
- 程序不变式被违反
- 关键配置缺失
- 程序状态不一致
- 测试中的预期失败
- 程序启动时的错误
- 硬件错误
一般panic有两种情况,一种是用户手动写panic!宏抛出错误,另外一种是程序运行中发生了不可恢复的错误,自动抛出的。这就要求程序员在编写程序的时候能够识别出可能发生错误的地方,并给出对应的处理措施。
11.1.1 手动触发panic
rust
fn main() {
//最简单的方式
panic!("出错了!");
}
这是手动抛出panic的最简单方式,直接调用panic!宏抛出。下面的示例是格式化输出错误信息。
11.1.2 系统自动抛出panic
rust
fn main() {
let z = divide(5, 0);
println!("{z}");
}
fn divide(x: i32, y: i32) -> i32 {
x / y
}
上面的示例中除数y被传入参数0,导致系统自动抛出panic。
rust
fn main() {
let arr = vec![1, 2, 3];
let r = get_element(&arr, 10);
println!("{r}");
}
fn get_element(arr: &[i32], index: usize) -> i32 {
arr[index]
}
上面的示例中由于访问了数组不存在的下标索引值,因此系统会报错。
11.1.3 panic的展开(unwinding)和终止(about)
这是Rust编程语言对panic的两种处理策略,默认使用展开,可以在Cargo.toml文件中配置为终止,如下所示:
rust
// 在 Cargo.toml 中配置:
[profile.release]
panic = 'abort' // 发布版本使用终止策略,减小二进制大小
fn main() {
// Rust默认使用展开(unwinding),但是可以配置为终止(abort)
// 在Cargo.toml文件中配置:
// [profile.release]
// panic='abort' //发布版本使用终止策略,减少二进制大小
demonstrate_unwinding();
}
fn demonstrate_unwinding() {
println!("函数开始");
let v = vec![1..100];
//let element = v[1000];
panic!("数组下标超过了!");
println!("这一行不会被执行");
}
使用 cargo run --release编译上面的代码,会减少代码的大小。
展开过程的展示
rust
fn layer3() {
println!("进入layer3");
panic!("layer3中的错误");
println!("离开layer3");
}
fn layer2() {
println!("进入layer2");
layer3();
println!("离开layer2");
}
fn layer1() {
println!("进入layer1");
layer2();
println!("离开layer1");
}
fn main() {
use std::panic;
println!("测试主程序开始!");
let result = panic::catch_unwind(|| {
layer1();
});
match result {
Ok(_) => println!("程序正常运行完成!"),
Err(_) => println!("捕获到panic,程序继续执行"),
}
println!("程序运行结束!");
}
11.1.4 适合使用panic的场景-测试场景
有时测试场景下,只需要捕获的错误就可以终止程序的执行了,可以使用panic。
rust
pub fn divide(left: u64, right: u64) -> u64 {
left / right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divide_by_zero() {
let result = divide(12, 0);
assert_eq!(result, 4);
}
#[test]
fn test_setup_config() {
setup_config();
}
#[test]
fn test_circle_new(){
let cirlce = Circle::new(-10.8);
}
#[test]
fn test_circle_set_radius(){
let mut circle = Circle::new(10.1);
circle.set_radius(15.2);
}
}
fn setup_config() {
let required_env="DATABASE_URL";
match std::env::var(required_env) {
Ok(url) => {
if url.is_empty() {
panic!("环境变量{required_env}不能为空");
}
println!("数据库url是:{required_env}");
}
Err(_) => {
panic!("当前环境没有设置环境变量:{required_env}");
}
}
}
pub struct Circle {
radius: f64,
}
impl Circle {
fn new(radius:f64)->Circle {
assert!(radius>0.0,"半径必须大于零:{}",radius);
Circle{radius}
}
fn set_radius(&mut self, radius: f64) {
let old_radius = self.radius;
self.radius = radius;
assert!{self.radius>0.0,"半径必须大于零:{}",radius};
}
}
panic不适合用于出现错误后,还需要继续系统的正常运行时场景。这类错误应该使用Result或Option枚举。
- 11.1.5 panic相关的宏和函数
- assert序列宏
rust
fn process_data(value: i32) {
//基本断言
assert!(value >= 0, "值不能为负数{value}");
//相等断言
let expected = 42;
assert_eq!(value, expected, "期待{expected},实际{value}");
//不相等断言
assert_ne!(value, 0, "值不能为0");
//调试断言(只在调试编译时检查)
debug_assert!(value < 1000, "值过大:{value}");
}
fn divide_assert(a: i32, b: i32) -> i32 {
assert!(b != 0, "除数不能为0");
a / b
}
fn main() {
println!("10/5={}", divide_assert(10, 5));
process_data(42);
println!("10/0={}", divide_assert(10, 0));
process_data(20);
process_data(-20);
process_data(10020);
}
程序演示了assert的四种用法,分别是:
- 基本断言
- 相等断言
- 不相等断言
- 提示断言
- unreachable和unimplemented宏
rust
fn handle_status(code: u32) -> String {
match code {
200 => "OK".to_string(),
404 => "Not Found".to_string(),
500 => "Internal Server Error".to_string(),
_ => unreachable!("不应该到达这里:code={code}"),
}
}
//占位实现
fn process_payment(amount: f64) -> Result<(), String> {
//开发中临时使用
//unimplemented("支付功能尚未实现,敬请期待!");
//或者
//todo!("请实现支付功能的处理逻辑");
//临时返回成功
Ok(())
}
fn main() {
process_payment(100.5);
handle_status(20);
}
上面的示例展示两种宏的使用场景:
- unreachable!:程序不应该到达此处
- umimplemented!:程序还没有完成该功能,只是作为占位符使用。
- 捕获和处理panic
rust
use std::panic;
fn risky_operation() -> i32 {
let numbers = vec![1, 2, 3, 4];
let index = 10;
if index >= numbers.len() {
panic!("索引越界:{}", index);
}
numbers[index]
}
fn safe_wrapper() -> Result<i32, String> {
let result = panic::catch_unwind(|| risky_operation());
match result {
Ok(value) => Ok(value),
Err(_) => Err("操作失败,已捕获panic".to_string()),
}
}
fn main() {
match safe_wrapper() {
Ok(value) => println!("结果:{value}"),
Err(e) => println!("错误:{e}"),
}
}
使用catch_unwind函数可以捕获panic,阻止程序中断,并对其错误做出对应的处理。
11.2 可恢复的错误使用Result
在Rust中,绝大部分错误都是可以恢复或跳过的,不需要终止程序的执行,因此需要一种可以恢复的错误,并且抛出错误的原因。例如:尝试打开一个文件,如果该文件不存在,可以尝试新建一个文件。Result枚举类型有此而生。
rust
enum Result<T,E>{
Ok(T), // 成功时返回的值
Err(E), // 失败时返回的值
}
T:成功时返回的值的类型(例如文件的句柄std::fs::File)。
E:失败时返回值的类型(例如 std::io::Error)。
11.2.1 对Result枚举的处理方式:match
当你调用文件打开的处理函数时(File::open),将会有两种可能性,成功或失败,而Result可以同时代表这两种状态。match根据Result的返回类型来进行相应的处理。
rust
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc, //文件不存在就创建一个新文件
Err(e) => panic!("无法创建文件:{:?}", e),
},
other_error => {
panic!("无法打开文件:{:?}", other_error);
}
},
};
println!("程序运行完毕!");
}
解析:
- 外层match:判断File::open执行的结果。
- Ok(file)分支:操作成功,直接使用句柄。
- Err(error)分支:操作失败,进一步分析原因。
- 内层match:使用error.kind()判断具体错误类型(如NotFound),并采取不同的补救措施(如创建文件)
11.2.2 使用unwrap和expect简化代码
使用match可以进行精确的匹配结果,但是代码显得非常冗长。Rust提供了两种快捷方式:
- unwrap()
行为:如果结果是Ok,返回其值,如果是Err,直接调用panic!宏。
使用场景:确定操作绝对不会失败,或者失败是直接崩溃也无所谓的情况。
rust
let greeting_file = File::open("hello.txt").unwrap();
- expect()推荐使用
行为:与unwrap()类似,但是允许你自定义panic!时的报错信息。
适用场景:生产代码中,为了更好的可读性和调试体验,建议优先使用expect。
rust
let greeting_file = File::open("hello.txt").expect("hello.txt没有找到该文件!");
11.2.2 传播错误:?运算符
当一个函数内部的操作失败时,通常更好的做法是将错误信息返回给调用者,让调用者决定如何处理。这种机制称之为"传播错误"。
- 传统写法
rust
//传统写法
fn read_username_from_file() -> Result<String, std::io::Error> {
let mut f = match File::open("username.txt") {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
let result = match f.read_to_string(&mut s) {
Ok(username) => Ok(s),
Err(e) => Err(e),
};
result
}
- 使用?运算符的写法(更加简洁)
rust
//使用?操作符
fn read_username_from_file2() -> Result<String, std::io::Error> {
let mut f = File::open("username.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
?操作符的工作原理:
如果值是Ok(v),它进行解包并返回v。
如果结果是Err(e),它立即从当前函数返回Err(e),实现错误的向上传播。
- 链式调用(终极简写)
rust
//链式调用(终极简写)
fn read_username_from_file3() -> Result<String, std::io::Error> {
let mut s = String::new();
File::open("username.txt")?.read_to_string(&mut s)?;
Ok(s)
}
- 使用标准库函数
rust
//使用标准库函数
fn read_username_from_file4() -> Result<String, std::io::Error> {
std::fs::read_to_string("username.txt")
}
- ?操作符的使用限制
注意:?操作符只能用在返回类型为Result和Option的函数中使用。如果函数的返回类型不是,则编译器会报错。
rust
fn main() {
use std::fs::File;
//编译错误!main函数默认返回空元祖类型。
let f = File::open("hello.txt!")?;
println!("程序运行完成!");
}
解决方案,修改main函数的签名,使其返回Result类型。
rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::fs::File;
let f = File::open("hello.txt")?;
Ok(())
}
11.3 何时 panic!,何时 Result**?**
|-----------------------|--------------------|---------------|
| 场景 | 推荐方式 | 理由 |
| 示例、原型代码 | unwrap()/ expect() | 快速编写,关注核心逻辑 |
| 可恢复错误(文件不存在、网络超时) | Result+ ? | 让调用者决定如何处理 |
| 不可恢复错误(Bug、逻辑错误) | panic! | 程序状态已损坏,应立即停止 |
| 库的公共 API | Result | 给予调用者最大的控制权 |
通过掌握 Result和 ?运算符,你将能写出既安全又符合 Rust 生态习惯的健壮代码。