
调试就像侦探破案——编译器给你线索,你要学会解读,然后找出那个"罪犯"(bug)!
凌晨 2 点,你对着屏幕上的代码:
fn process_data(data: Vec<i32>) -> i32 {
let mut sum = ;
for i in ..data.len() {
sum += data[i + ]; // 🤔 为啥结果不对?
}
sum
}
你: "这代码逻辑没问题啊!"
编译器: "borrowed value does not live long enough"
你: "???我只是加了个数而已!"
编译器: "我不允许!"
调试是每个程序员的日常,而 Rust 的调试...有点特别。它不像 Python 那样可以随便 print(),也不像 Java 那样有成熟的 IDE 调试器支持(虽然现在好多了)。
今天咱们就聊聊 Rust 调试的那些事儿,从最原始的 println! 到专业的 gdb/lldb,再到 IDE 神器!
阶段 1:println! 调试
↓
阶段 2:日志调试(tracing/env_logger)
↓
阶段 3:专业调试器(gdb/lldb/IDE)
生活化类比:
调试方法 | 类比 | 适用场景 |
|---|---|---|
println! | 用便利贴做标记 | 快速定位,小问题 |
日志 | 装监控摄像头 | 生产环境,需要记录 |
调试器 | 请侦探来破案 | 复杂 bug,内存问题 |
断言 | 安检门 | 确保前提条件 |
调试不是"试",而是"推理":
💡 记住: 编译器错误信息不是天书,它在帮你!
别看不起 println!,它是最常用的调试工具!
fn calculate_sum(numbers: Vec<i32>) -> i32 {
let mut sum = ;
// 调试:打印输入
println!("[DEBUG] 输入:{:?}", numbers);
for (i, &num) in numbers.iter().enumerate() {
sum += num;
// 调试:打印每一步
println!("[DEBUG] 步骤 {}: num = {}, sum = {}", i, num, sum);
}
println!("[DEBUG] 最终结果:{}", sum);
sum
}
fn main() {
let data = vec![, , , , ];
let result = calculate_sum(data);
println!("结果:{}", result);
}
输出:
[DEBUG] 输入:[1, 2, 3, 4, 5]
[DEBUG] 步骤 0: num = 1, sum = 1
[DEBUG] 步骤 1: num = 2, sum = 3
[DEBUG] 步骤 2: num = 3, sum = 6
[DEBUG] 步骤 3: num = 4, sum = 10
[DEBUG] 步骤 4: num = 5, sum = 15
[DEBUG] 最终结果:15
结果:15
💡 调试技巧:
// 1. 打印变量名 + 值(Rust 1.58+ 支持)
let x = ;
println!("{x = ?}"); // 输出:x = 42
// 2. 条件打印(只在特定条件下打印)
if cfg!(debug_assertions) {
println!("调试信息:{}", value);
}
// 3. 打印后退出(定位崩溃位置)
println!("程序运行到这里了!");
std::process::exit();
// 4. dbg! 宏(更简洁的调试)
let x = ;
let y = dbg!(x * ); // 打印表达式并返回值
// 输出:[src/main.rs:3] x * 2 = 20
dbg! 宏真香示例:
fn process(value: i32) -> i32 {
// 传统方式
println!("value = {}", value);
let result = value * ;
println!("result = {}", result);
result
// 使用 dbg!
dbg!(value);
let result = dbg!(value * );
result
}
安装 tracing:
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
use tracing::{info, warn, error, debug, instrument};
use tracing_subscriber;
// 日志级别(从低到高):
// TRACE < DEBUG < INFO < WARN < ERROR
#[instrument(name = "处理用户数据", skip(user_data))]
fn process_user(user_data: &str) -> Result<String, String> {
debug!("开始处理用户数据");
if user_data.is_empty() {
warn!("用户数据为空");
return Err("数据不能为空".to_string());
}
info!("处理数据:{}", user_data);
// 模拟处理
let result = user_data.to_uppercase();
debug!("处理完成");
Ok(result)
}
fn main() {
// 初始化日志
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
match process_user("Larry") {
Ok(result) => info!("成功:{}", result),
Err(e) => error!("失败:{}", e),
}
// 测试错误情况
let _ = process_user("");
}
输出:
2024-01-15T10:30:00.123456Z DEBUG 处理用户数据:开始处理用户数据
2024-01-15T10:30:00.123789Z INFO 处理用户数据:处理数据:Larry
2024-01-15T10:30:00.124012Z DEBUG 处理用户数据:处理完成
2024-01-15T10:30:00.124234Z INFO 成功:LARRY
2024-01-15T10:30:00.124456Z DEBUG 处理用户数据:开始处理用户数据
2024-01-15T10:30:00.124678Z WARN 处理用户数据:用户数据为空
2024-01-15T10:30:00.124890Z ERROR 失败:数据不能为空
💡 日志级别使用场景:
级别 | 使用场景 | 示例 |
|---|---|---|
ERROR | 系统错误 | 数据库连接失败 |
WARN | 警告但不影响运行 | 配置缺失,使用默认值 |
INFO | 重要事件 | 用户登录、订单创建 |
DEBUG | 调试信息 | 函数入口/出口、参数值 |
TRACE | 详细追踪 | 循环每次迭代 |
断言是代码的"安检门":
fn divide(a: f64, b: f64) -> f64 {
// 前置条件检查
assert!(b != 0.0, "除数不能为零!");
assert!(a.is_finite(), "被除数必须是有限值");
assert!(b.is_finite(), "除数必须是有限值");
a / b
}
fn process_array(arr: &[i32], index: usize) -> i32 {
// 调试断言(只在 debug 模式生效)
debug_assert!(!arr.is_empty(), "数组不能为空");
debug_assert!(index < arr.len(), "索引越界");
arr[index]
}
fn main() {
// 这个会 panic
let result = divide(10.0, 0.0);
// thread 'main' panicked at '除数不能为零!', src/main.rs:4:5
}
assert! vs debug_assert!:
// assert! - 所有模式都检查
assert!(x > ); // 生产环境也会检查,影响性能
// debug_assert! - 只在 debug 模式检查
debug_assert!(x > ); // release 模式会被优化掉
第一步:编译时包含调试信息
# Cargo 默认在 debug 模式包含调试信息
cargo build
# 或者手动指定
cargo build --profile dev
第二步:找到可执行文件
# debug 模式的可执行文件位置
# Linux/Mac: target/debug/your_app
# Windows: target\debug\your_app.exe
第三步:使用 lldb(Mac)或 gdb(Linux)
lldb 示例:
# 启动 lldb
lldb target/debug/my_app
# 常用命令
(lldb) breakpoint set --name main # 在 main 函数设断点
(lldb) run # 运行
(lldb) next # 下一步
(lldb) step # 进入函数
(lldb) print variable_name # 打印变量
(lldb) continue# 继续运行
(lldb) quit # 退出
gdb 示例:
# 启动 gdb
gdb target/debug/my_app
# 常用命令
(gdb) break main # 在 main 函数设断点
(gdb) run # 运行
(gdb) next # 下一步
(gdb) step # 进入函数
(gdb) print variable_name # 打印变量
(gdb) backtrace # 查看调用栈
(gdb) continue# 继续运行
(gdb) quit # 退出
调试 Rust 的特殊性:
Rust 的调试器支持还在完善中,有些类型可能显示不完整。遇到这种情况:
// 方法 1:用 println! 辅助
println!("{:?}", my_complex_variable);
// 方法 2:用 dbg!
dbg!(&my_complex_variable);
安装扩展:
调试配置(.vscode/launch.json):
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"cargo": {
"args": ["build", "--bin", "my_app"]
},
"cwd": "${workspaceFolder}"
}
]
}
VS Code 调试功能:
优势:
使用方法:
问题:
cargo run --release
# 然后发现调试器里变量都显示"optimized out"
原因: Release 模式做了优化,变量可能被优化掉
解决: 用 debug 模式调试
cargo run
# 或者
cargo build
问题: 代码里到处都是调试输出
解决:
// 方法 1:用 cfg 条件编译
#[cfg(debug_assertions)]
println!("调试信息:{}", value);
// 方法 2:用日志级别控制
tracing::debug!("调试信息:{}", value);
// 生产环境设置日志级别为 INFO 就不会打印 DEBUG
// 方法 3:用 git 搜索
git grep "println!" # 找出所有调试输出
问题: 有时正常,有时崩溃
解决:
// 1. 增加日志,记录线程执行顺序
tracing::info!("线程 {:?} 进入临界区", std::thread::current().id());
// 2. 使用 Thread Sanitizer(需要 nightly)
// RUSTFLAGS="-Z thread-sanitizer" cargo run
// 3. 增加测试次数
for i in .. {
test_concurrent_code();
}
// 4. 使用 loom 测试并发
// [dev-dependencies]
// loom = "0.7"
错误信息:
error[E0597]: `x` does not live long enough
--> src/main.rs:10:5
|
10 | let r = &x;
| ------^^--
| | |
| | borrowed value does not live long enough
| a temporary with access to the borrow has a lifetime of '2
翻译成人话:
"你想借
x,但x比你借的地方活得短。就像你想借朋友的手机,但朋友在你还之前就出国了..."
解决思路:
问题代码:
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
}
fn create_cycle() {
let a = Rc::new(RefCell::new(Node { value: , next: None }));
let b = Rc::new(RefCell::new(Node { value: , next: None }));
// 制造循环引用
a.borrow_mut().next = Some(Rc::clone(&b));
b.borrow_mut().next = Some(Rc::clone(&a)); // 🐛 这里!
}
fn main() {
create_cycle();
println!("程序结束");
// 内存泄漏:a 和 b 互相引用,引用计数永远不为 0
}
调试步骤:
步骤 1:用 println! 定位
fn create_cycle() {
let a = Rc::new(RefCell::new(Node { value: , next: None }));
let b = Rc::new(RefCell::new(Node { value: , next: None }));
println!("创建后:a 的引用计数 = {}", Rc::strong_count(&a));
println!("创建后:b 的引用计数 = {}", Rc::strong_count(&b));
a.borrow_mut().next = Some(Rc::clone(&b));
println!("a 指向 b 后:a 的引用计数 = {}", Rc::strong_count(&a));
println!("a 指向 b 后:b 的引用计数 = {}", Rc::strong_count(&b));
b.borrow_mut().next = Some(Rc::clone(&a));
println!("b 指向 a 后:a 的引用计数 = {}", Rc::strong_count(&a));
println!("b 指向 a 后:b 的引用计数 = {}", Rc::strong_count(&b));
}
输出:
创建后:a 的引用计数 = 1
创建后:b 的引用计数 = 1
a 指向 b 后:a 的引用计数 = 1
a 指向 b 后:b 的引用计数 = 2
b 指向 a 后:a 的引用计数 = 2 // 🚨 问题!
b 指向 a 后:b 的引用计数 = 2 // 🚨 问题!
步骤 2:分析原因
步骤 3:修复
use std::cell::RefCell;
use std::rc::{Rc, Weak}; // 用 Weak 打破循环
#[derive(Debug)]
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
prev: Option<Weak<RefCell<Node>>>, // 用 Weak 引用
}
fn create_cycle() {
let a = Rc::new(RefCell::new(Node { value: , next: None, prev: None }));
let b = Rc::new(RefCell::new(Node { value: , next: None, prev: None }));
a.borrow_mut().next = Some(Rc::clone(&b));
b.borrow_mut().prev = Some(Rc::downgrade(&a)); // 用 Weak
}

核心要点:
金句时间:
"调试不是承认失败,而是理解代码的过程。" "好的调试技能 = 50% 的工具使用 + 50% 的逻辑推理"
下篇预告: 第 50 篇(最终篇)咱们来聊聊发布与部署 - 怎么把 Rust 程序发布到各种平台?Docker 怎么配?CI/CD 怎么做?学完 Rust 后的学习路线是什么?敬请期待!