跳到主要内容

panic! 和错误处理策略

在 Rust 中,有两种主要的错误处理方式:可恢复的错误(使用 Result<T, E>)和不可恢复的错误(使用 panic!)。

什么是 panic!

panic! 是 Rust 中处理不可恢复错误的机制。当程序遇到无法继续执行的情况时,会触发 panic。

显式调用 panic!

fn main() {
panic!("程序崩溃了!");
}

常见的 panic 触发情况

fn main() {
// 数组越界访问
let v = vec![1, 2, 3];
// v[99]; // 这会触发 panic!

// 除零操作(对于整数)
// let result = 10 / 0; // 这会触发 panic!

// unwrap() 在 None 或 Err 上
let x: Option<i32> = None;
// x.unwrap(); // 这会触发 panic!

// 断言失败
// assert_eq!(2 + 2, 5); // 这会触发 panic!

println!("程序正常运行");
}

panic! 的行为

当 panic 发生时,程序会:

  1. 打印错误信息
  2. 展开(unwind)并清理栈
  3. 退出程序

设置 panic 行为

Cargo.toml 中可以配置 panic 的行为:

[profile.release]
panic = 'abort' # 直接终止,不进行栈展开

使用 panic! 的场景

1. 程序逻辑错误

fn calculate_average(numbers: &[i32]) -> f64 {
if numbers.is_empty() {
panic!("不能计算空数组的平均值");
}

let sum: i32 = numbers.iter().sum();
sum as f64 / numbers.len() as f64
}

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
println!("平均值: {}", calculate_average(&numbers));

// 这会触发 panic
// let empty: Vec<i32> = vec![];
// calculate_average(&empty);
}

2. 不变量违反

struct PositiveNumber {
value: u32,
}

impl PositiveNumber {
fn new(value: u32) -> PositiveNumber {
if value == 0 {
panic!("PositiveNumber 必须大于零");
}
PositiveNumber { value }
}

fn get(&self) -> u32 {
self.value
}
}

fn main() {
let num = PositiveNumber::new(42);
println!("正数: {}", num.get());

// 这会触发 panic
// let zero = PositiveNumber::new(0);
}

捕获 panic

虽然 panic 通常会终止程序,但可以使用 std::panic::catch_unwind 来捕获:

use std::panic;

fn might_panic(should_panic: bool) {
if should_panic {
panic!("故意触发的 panic!");
}
println!("没有 panic");
}

fn main() {
// 正常执行
might_panic(false);

// 捕获 panic
let result = panic::catch_unwind(|| {
might_panic(true);
});

match result {
Ok(_) => println!("没有发生 panic"),
Err(_) => println!("捕获到 panic"),
}

println!("程序继续执行");
}

错误处理策略

1. 何时使用 panic!

  • 程序逻辑错误:不应该发生的情况
  • 不变量违反:数据结构的约束被破坏
  • 原型和示例代码:快速开发时
  • 测试代码:验证错误条件
// 好的 panic 使用场景
fn get_element(slice: &[i32], index: usize) -> i32 {
if index >= slice.len() {
panic!("索引 {} 超出范围,数组长度为 {}", index, slice.len());
}
slice[index]
}

// 更好的做法:返回 Option
fn get_element_safe(slice: &[i32], index: usize) -> Option<i32> {
slice.get(index).copied()
}

fn main() {
let numbers = vec![1, 2, 3];

// 在确定安全的情况下使用 panic 版本
println!("第一个元素: {}", get_element(&numbers, 0));

// 在不确定的情况下使用安全版本
match get_element_safe(&numbers, 5) {
Some(value) => println!("值: {}", value),
None => println!("索引超出范围"),
}
}

2. 何时使用 Result

  • 可预期的错误:文件不存在、网络连接失败等
  • 用户输入错误:解析失败、格式错误等
  • 外部依赖错误:数据库连接、API 调用等
  • 库代码:让调用者决定如何处理错误
use std::fs::File;
use std::io::{self, Read};

// 使用 Result 处理可预期的错误
fn read_config_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}

// 解析配置的函数
fn parse_config(content: &str) -> Result<Config, ConfigError> {
// 解析逻辑...
Ok(Config::default())
}

#[derive(Default)]
struct Config;

#[derive(Debug)]
enum ConfigError {
InvalidFormat,
}

impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "配置格式无效")
}
}

impl std::error::Error for ConfigError {}

fn main() {
match read_config_file("config.txt") {
Ok(content) => {
match parse_config(&content) {
Ok(_config) => println!("配置加载成功"),
Err(e) => println!("配置解析失败: {}", e),
}
}
Err(e) => println!("读取配置文件失败: {}", e),
}
}

自定义 panic 钩子

可以设置自定义的 panic 钩子来控制 panic 的行为:

use std::panic;

fn main() {
// 设置自定义 panic 钩子
panic::set_hook(Box::new(|panic_info| {
println!("自定义 panic 处理器被调用!");

if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
println!("panic 消息: {}", s);
}

if let Some(location) = panic_info.location() {
println!("panic 发生在文件 '{}' 的第 {} 行",
location.file(), location.line());
}
}));

println!("即将触发 panic...");
panic!("这是一个测试 panic");
}

错误处理的最佳实践

1. 分层错误处理

// 底层:具体的错误类型
#[derive(Debug)]
enum DatabaseError {
ConnectionFailed,
QueryFailed(String),
}

// 中层:业务逻辑错误
#[derive(Debug)]
enum UserServiceError {
Database(DatabaseError),
UserNotFound(u32),
InvalidInput(String),
}

// 顶层:应用程序错误
#[derive(Debug)]
enum AppError {
UserService(UserServiceError),
Authentication,
Authorization,
}

impl From<DatabaseError> for UserServiceError {
fn from(err: DatabaseError) -> Self {
UserServiceError::Database(err)
}
}

impl From<UserServiceError> for AppError {
fn from(err: UserServiceError) -> Self {
AppError::UserService(err)
}
}

fn find_user(id: u32) -> Result<String, DatabaseError> {
if id == 0 {
Err(DatabaseError::QueryFailed("无效的用户 ID".to_string()))
} else {
Ok(format!("用户 {}", id))
}
}

fn get_user_profile(id: u32) -> Result<String, UserServiceError> {
let user = find_user(id)?;
Ok(format!("{}的个人资料", user))
}

fn handle_request(user_id: u32) -> Result<String, AppError> {
let profile = get_user_profile(user_id)?;
Ok(profile)
}

fn main() {
match handle_request(0) {
Ok(profile) => println!("成功: {}", profile),
Err(error) => println!("错误: {:?}", error),
}
}

2. 错误恢复策略

use std::thread;
use std::time::Duration;

fn unreliable_operation() -> Result<String, &'static str> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

let mut hasher = DefaultHasher::new();
std::ptr::addr_of!(hasher).hash(&mut hasher);

if hasher.finish() % 3 == 0 {
Ok("操作成功".to_string())
} else {
Err("操作失败")
}
}

fn retry_operation(max_attempts: u32) -> Result<String, String> {
for attempt in 1..=max_attempts {
match unreliable_operation() {
Ok(result) => return Ok(result),
Err(error) => {
if attempt == max_attempts {
return Err(format!("在 {} 次尝试后仍然失败: {}", max_attempts, error));
}
println!("第 {} 次尝试失败,重试中...", attempt);
thread::sleep(Duration::from_millis(100));
}
}
}
unreachable!()
}

fn main() {
match retry_operation(3) {
Ok(result) => println!("成功: {}", result),
Err(error) => println!("最终失败: {}", error),
}
}

3. 优雅降级

struct CacheService {
available: bool,
}

impl CacheService {
fn new() -> Self {
CacheService { available: true }
}

fn get(&self, key: &str) -> Option<String> {
if self.available {
// 模拟缓存查找
if key == "user:1" {
Some("张三".to_string())
} else {
None
}
} else {
None
}
}
}

struct DatabaseService;

impl DatabaseService {
fn get_user(&self, id: u32) -> Result<String, &'static str> {
if id == 1 {
Ok("张三".to_string())
} else {
Err("用户不存在")
}
}
}

struct UserService {
cache: CacheService,
database: DatabaseService,
}

impl UserService {
fn new() -> Self {
UserService {
cache: CacheService::new(),
database: DatabaseService,
}
}

fn get_user(&self, id: u32) -> Result<String, &'static str> {
let cache_key = format!("user:{}", id);

// 首先尝试从缓存获取
if let Some(user) = self.cache.get(&cache_key) {
println!("从缓存获取用户");
return Ok(user);
}

// 缓存未命中,从数据库获取
println!("从数据库获取用户");
self.database.get_user(id)
}
}

fn main() {
let service = UserService::new();

match service.get_user(1) {
Ok(user) => println!("用户: {}", user),
Err(error) => println!("错误: {}", error),
}
}

总结

选择正确的错误处理策略:

  1. 使用 panic! 当:

    • 程序遇到不可恢复的错误
    • 违反了程序的不变量
    • 在原型开发阶段
  2. 使用 Result 当:

    • 错误是可预期和可恢复的
    • 调用者可能想要处理错误
    • 编写库代码
  3. 使用 Option 当:

    • 值可能存在也可能不存在
    • 这是正常的业务逻辑
  4. 错误处理的原则

    • 让错误显式化
    • 在适当的层级处理错误
    • 提供有意义的错误信息
    • 考虑错误恢复策略