Skip to content

Ch 4: std::panic - 恐慌与恢复

panic 是 Rust 中用于处理不可恢复错误的机制。当程序遇到无法继续执行的情况时,会触发 panic。本章将详细讲解 panic 的工作原理、如何捕获 panic、以及如何自定义 panic 处理行为。

panic! 宏用于主动触发 panic,通常用于表示程序遇到了无法恢复的错误:

基本用法:

fn main() {
println!("程序开始...");
panic!("这是一个恐慌!");
println!("这行不会执行");
}

运行结果:

thread 'main' panicked at '这是一个恐慌!', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Rust 程序在 panic 时可以选择” unwind “或” abort “两种终止方式:

  1. Unwind(默认):展开调用栈,调用所有局部变量的析构函数。
  2. Abort:直接终止程序,不展开栈。
// Cargo.toml 中可以设置
[profile.release]
panic = "abort" # 发布版本默认 abort

assert! 是基于 panic! 的断言宏:

基本用法:

fn main() {
let x = 5;
assert!(x > 0, "x 应该是正数,但它是 {}", x);
let v = vec![1, 2, 3];
assert_eq!(v.len(), 3, "向量长度应该是 3");
}

标准库提供的断言宏:

// assert_eq! - 断言相等
assert_eq!(2 + 2, 4);
// assert_ne! - 断言不等
assert_ne!(2 + 2, 5);
// debug_assert! - 仅在调试模式生效
debug_assert!(true);

std::panic::catch_unwind 允许捕获 panic,使程序不会终止:

方法签名:

pub fn catch_unwind<F, T>(f: F) -> Result<T, Box<dyn Any + Send>>
where
F: FnOnce() -> T + UnwindSafe,

示例:

use std::panic;
fn may_panic() {
println!("即将 panic...");
panic!("Oops!");
}
fn main() {
let result = panic::catch_unwind(may_panic);
match result {
Ok(_) => println!("没有 panic"),
Err(e) => {
println!("捕获到 panic: {:?}", e);
}
}
println!("程序继续执行");
}

注意返回值的类型:

use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
panic!("something went wrong");
});
if result.is_err() {
println!("捕获到 panic");
}
// 可以使用 downcast 获取 panic 信息
let result = panic::catch_unwind(|| {
panic!("error code: {}", 42);
});
if let Err(e) = result {
if let Some(s) = e.downcast_ref::<&str>() {
println!("panic message: {}", s);
}
}
}

AssertUnwindSafe 用于标记在 panic 后仍可以安全使用的值:

使用场景 - 与 catch_unwind 配合:

use std::panic::{catch_unwind, AssertUnwindSafe};
fn main() {
let mut data = vec![1, 2, 3];
// 如果闭包可能 panic,但你想在 catch 后继续使用数据
let result = catch_unwind(AssertUnwindSafe(|| {
data.push(4);
if data.len() > 10 {
panic!("Too many elements!");
}
}));
println!("result: {:?}", result); // Ok(()) 或 Err(...)
// 正常情况下 data 仍然可用
println!("data: {:?}", data);
}

为什么不默认是 UnwindSafe:

use std::panic::{catch_unwind, UnwindSafe};
struct NotThreadSafe {
data: Vec<u32>,
}
impl UnwindSafe for NotThreadSafe {}

set_hook 允许你自定义 panic 发生时的行为:

基本用法:

use std::panic;
fn main() {
// 保存原来的 hook
let original_hook = panic::take_hook();
// 设置新的 hook
panic::set_hook(Box::new(|panic_info| {
let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
let location = panic_info.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "unknown location".to_string());
eprintln!("[PANIC] {} at {}", msg, location);
}));
panic!("Test panic");
// 恢复原来的 hook
panic::set_hook(original_hook);
}

将 panic 信息写入日志:

use std::panic;
use std::fs::OpenOptions;
use std::io::Write;
fn setup_panic_handler() {
panic::set_hook(Box::new(|panic_info| {
let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else {
"Unknown".to_string()
};
let location = panic_info.location()
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
.unwrap_or_else(|| "unknown".to_string());
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let log_line = format!("[{}] PANIC at {}: {}\n", timestamp, location, msg);
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open("panic.log")
{
let _ = file.write_all(log_line.as_bytes());
}
}));
}
fn main() {
setup_panic_handler();
panic!("Test");
}

PanicInfo 包含 panic 的详细信息:

use std::panic;
fn main() {
panic::set_hook(Box::new(|panic_info| {
// payload - panic 携带的数据
if let Some(msg) = panic_info.payload().downcast_ref::<&str>() {
println!("Payload: {}", msg);
}
// location - panic 发生的位置
if let Some(loc) = panic_info.location() {
println!("File: {}", loc.file());
println!("Line: {}", loc.line());
println!("Column: {}", loc.column());
}
}));
panic!("Error at some point");
}

resume_unwind 用于重新触发被捕获的 panic:

use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
panic!("original panic");
});
if let Err(e) = result {
println!("捕获到 panic,继续传播");
// 重新触发 panic
resume_unwind(e);
}
}
  1. panic! 不是用于错误处理:正常情况下应使用 Result 和 Option,只有在真正无法恢复时才使用 panic!。

  2. catch_unwind 有性能开销:每次捕获 panic 都需要展开栈,在性能敏感路径上要谨慎使用。

  3. Rust 默认是 unwind:发布版本可以切换为 abort 以减小二进制大小。

  4. panic! 会触发析构函数:unwind 时会调用所有局部变量的析构函数。

  5. catch_unwind 只能捕获 unwind:如果程序设置为 abort 模式,catch_unwind 无法捕获。

  6. UnwindSafe trait:某些类型在 panic 后可能处于不一致状态,Rust 用 UnwindSafe 标记来帮助检测。

  1. 编写一个程序,使用 catch_unwind 捕获可能 panic 的计算。
  2. 实现一个自定义的 panic hook,将 panic 信息写入文件。
  3. 研究 panic=abort 对程序行为的影响。
  4. 实现一个函数,用 catch_unwind 包装一个可能 panic 的 FFI 调用。

std::panic 模块提供了 Rust 恐慌处理的核心功能:

组件作用
panic!触发 panic
catch_unwind捕获 panic
AssertUnwindSafe标记线程安全的值
PanicInfopanic 详细信息
set_hook自定义 panic 处理器
resume_unwind继续传播 panic

理解 panic 机制对于编写健壮的 Rust 程序至关重要,特别是在编写库或与外部代码交互时。