面向 C/C++ 程序员的 Rust 入门课程
版权与署名说明(非官方译本) 本文档为 microsoft/RustTraining 中
c-cpp-book的中文翻译与改编版本。 译文主要由 AI 生成,未经过人工校对与修订。 原作版权归 Microsoft Corporation 所有;文档许可为 CC-BY-4.0。 本译本为社区非官方版本,不代表 Microsoft 官方立场。
课程概览
- 课程概览
- 选择 Rust 的理由(从 C 和 C++ 两个视角)
- 本地安装
- 类型、函数、控制流、模式匹配
- 模块、Cargo
- Trait、泛型
- 集合、错误处理
- 闭包、内存管理、生命周期、智能指针
- 并发
- Unsafe Rust,包括外部函数接口(Foreign Function Interface,FFI)
no_std与面向固件团队的嵌入式 Rust 要点- 案例研究:真实世界的 C++ 到 Rust 翻译模式
- 本课程不涵盖
asyncRust——请参阅配套 Async Rust Training,了解 future、执行器、Pin、tokio 及生产级异步模式
自学指南
本材料既可作为讲师授课课程,也适合自学。如果你独自学习,以下建议可帮助你充分利用:
进度建议:
| 章节 | 主题 | 建议用时 | 检查点 |
|---|---|---|---|
| 1–4 | 环境搭建、类型、控制流 | 1 天 | 能编写 CLI 温度转换器 |
| 5–7 | 数据结构、所有权 | 1–2 天 | 能解释 为什么 let s2 = s1 会使 s1 失效 |
| 8–9 | 模块、错误处理 | 1 天 | 能创建多文件项目并用 ? 传播错误 |
| 10–12 | Trait、泛型、闭包 | 1–2 天 | 能编写带 Trait 约束的泛型函数 |
| 13–14 | 并发、unsafe/FFI | 1 天 | 能用 Arc<Mutex<T>> 编写线程安全计数器 |
| 15–16 | 深入专题 | 自定进度 | 参考资料——在需要时阅读 |
| 17–19 | 最佳实践与参考 | 自定进度 | 编写真实代码时查阅 |
如何使用练习:
- 每章都有动手练习,难度标注为:🟢 入门、🟡 中级、🔴 挑战
- 务必在展开答案前先尝试练习。 与借用检查器较劲是学习的一部分——编译器的错误信息就是你的老师
- 如果卡住超过 15 分钟,展开答案、研读后关闭,再从头试一次
- Rust Playground 无需本地安装即可运行代码
遇到瓶颈时:
- 仔细阅读编译器错误信息——Rust 的错误提示非常出色
- 重读相关章节;所有权(第 7 章)等概念往往在第二遍才豁然开朗
- Rust 标准库文档 质量很高——可搜索任意类型或方法
- 异步模式请参阅配套 Async Rust Training
目录
第一部分 — 基础
1. 引言与动机
2. 快速上手
3. 基本类型与变量
4. 控制流
5. 数据结构与集合
- Rust 数组类型
- Rust 元组
- Rust 引用
- C++ 引用 vs Rust 引用——关键差异
- Rust 切片
- Rust 常量与静态变量
- Rust 字符串:
Stringvs&str - Rust 结构体
- Rust
Vec<T> - Rust
HashMap - 练习:Vec 与 HashMap
6. 模式匹配与枚举
7. 所有权与内存管理
- Rust 内存管理
- Rust 所有权、借用与生命周期
- Rust 移动语义
- Rust
Clone - Rust
CopyTrait - Rust
DropTrait - 练习:Move、Copy 与 Drop
- Rust 生命周期与借用
- Rust 生命周期标注
- 练习:带生命周期的切片存储
- 生命周期省略规则深入
- Rust
Box<T> - 内部可变性:
Cell<T>与RefCell<T> - 共享所有权:
Rc<T> - 练习:共享所有权与内部可变性
8. 模块与 Crate
- Rust crate 与模块
- 练习:模块与函数
- 工作区与 crate(包)
- 练习:使用工作区与包依赖
- 使用 crates.io 社区 crate
- Crate 依赖与 SemVer
- 练习:使用 rand crate
- Cargo.toml 与 Cargo.lock
- Cargo 测试功能
- Cargo 其他功能
- 测试模式
9. 错误处理
- 将枚举与 Option 和 Result 联系起来
- Rust
Option类型 - Rust
Result类型 - 练习:使用 Option 实现 log() 函数
- Rust 错误处理
- 练习:错误处理
- 错误处理最佳实践
10. Trait 与泛型
- Rust Trait
- C++ 运算符重载 → Rust
std::opsTrait - 练习:Logger Trait 实现
- 何时使用 enum vs
dyn Trait - 练习:翻译前先思考
- Rust 泛型
- 练习:泛型
- 结合 Rust Trait 与泛型
- 数据类型中的 Rust Trait 约束
- 练习:Trait 约束与泛型
- Rust 类型状态模式与泛型
- Rust 建造者模式
11. 类型系统高级特性
12. 函数式编程
13. 并发
14. Unsafe Rust 与 FFI
第二部分 — 深入专题
15. no_std — 裸机 Rust
16. 案例研究:真实世界的 C++ 到 Rust 翻译
- 案例研究 1:继承层次 → 枚举分发
- 案例研究 2:
shared_ptr树 → Arena/索引模式 - 案例研究 3:框架通信 → 生命周期借用
- 案例研究 4:上帝对象 → 可组合状态
- 案例研究 5:Trait 对象——何时才合适
第三部分 — 最佳实践与参考
17. 最佳实践
18. C++ → Rust 语义深入
19. Rust 宏
讲师介绍与总体方法
你将学到: 课程结构、互动形式,以及熟悉的 C/C++ 概念如何映射到 Rust 等价物。本章设定预期,并为全书提供路线图。
- 讲师介绍
- Microsoft SCHIE(Silicon and Cloud Hardware Infrastructure Engineering,硅与云硬件基础设施工程)团队首席固件架构师
- 行业资深专家,专长于安全、系统编程(固件、操作系统、虚拟机监控程序)、CPU 与平台架构,以及 C++ 系统开发
- 2017 年起在 Rust 中编程(@AWS EC2),此后一直热爱这门语言
- 本课程尽可能保持互动
- 前提:你熟悉 C、C++,或两者皆会
- 示例刻意设计为将熟悉概念映射到 Rust 等价物
- 请随时提出澄清问题
- 讲师期待与团队持续交流
选择 Rust 的理由
想直接看代码? 跳转到 给我看代码
无论你来自 C 还是 C++,核心痛点相同:能干净编译却在运行时崩溃、损坏或泄漏的内存安全 bug。
- 超过 70% 的 CVE 由内存安全问题引起——缓冲区溢出、悬垂指针、释放后使用
- C++ 的
shared_ptr、unique_ptr、RAII 和移动语义是正确方向,但只是创可贴,不是根治——移动后使用、引用循环、迭代器失效和异常安全漏洞依然敞开 - Rust 提供你依赖的 C/C++ 级性能,同时带来编译期安全保证
📖 深入阅读: 请参阅 为什么 C/C++ 开发者需要 Rust,了解具体漏洞示例、Rust 消除问题的完整清单,以及 C++ 智能指针为何不够
Rust 如何解决这些问题?
缓冲区溢出与边界违规
- 所有 Rust 数组、切片和字符串都关联显式边界。编译器插入检查,确保任何边界违规导致运行时崩溃(Rust 术语中为 panic)——绝不会是未定义行为
悬垂指针与引用
- Rust 引入生命周期和借用检查,在编译期消除悬垂引用
- 没有悬垂指针,没有释放后使用——编译器不会让你写出这类代码
移动后使用
- Rust 的所有权系统使移动具有破坏性——一旦移动值,编译器拒绝让你再使用原值。没有僵尸对象,没有“有效但未指定状态“
资源管理
- Rust 的
DropTrait 是正确实现的 RAII——编译器在变量离开作用域时自动释放资源,并防止移动后使用——这是 C++ RAII 做不到的 - 无需 Rule of Five(无需定义拷贝构造、移动构造、拷贝赋值、移动赋值、析构函数)
错误处理
- Rust 没有异常。所有错误都是值(
Result<T, E>),使错误处理显式且体现在类型签名中
迭代器失效
- Rust 的借用检查器禁止在迭代集合时修改它。你无法写出困扰 C++ 代码库的那类 bug:
#![allow(unused)]
fn main() {
// Rust equivalent of erase-during-iteration: retain()
pending_faults.retain(|f| f.id != fault_to_remove.id);
// Or: collect into a new Vec (functional style)
let remaining: Vec<_> = pending_faults
.into_iter()
.filter(|f| f.id != fault_to_remove.id)
.collect();
}
数据竞争
- 类型系统通过
Send和SyncTrait 在编译期防止数据竞争
内存安全可视化
Rust 所有权——设计上安全
#![allow(unused)]
fn main() {
fn safe_rust_ownership() {
// Move is destructive: original is gone
let data = vec![1, 2, 3];
let data2 = data; // Move happens
// data.len(); // Compile error: value used after move
// Borrowing: safe shared access
let owned = String::from("Hello, World!");
let slice: &str = &owned; // Borrow — no allocation
println!("{}", slice); // Always safe
// No dangling references possible
/*
let dangling_ref;
{
let temp = String::from("temporary");
dangling_ref = &temp; // Compile error: temp doesn't live long enough
}
*/
}
}
graph TD
A[Rust Ownership Safety] --> B[Destructive Moves]
A --> C[Automatic Memory Management]
A --> D[Compile-time Lifetime Checking]
A --> E[No Exceptions — Result Types]
B --> B1["Use-after-move is compile error"]
B --> B2["No zombie objects"]
C --> C1["Drop trait = RAII done right"]
C --> C2["No Rule of Five needed"]
D --> D1["Borrow checker prevents dangling"]
D --> D2["References always valid"]
E --> E1["Result<T,E> — errors in types"]
E --> E2["? operator for propagation"]
style A fill:#51cf66,color:#000
style B fill:#91e5a3,color:#000
style C fill:#91e5a3,color:#000
style D fill:#91e5a3,color:#000
style E fill:#91e5a3,color:#000
内存布局:Rust 引用
graph TD
RM1[Stack] --> RP1["&i32 ref"]
RM2[Stack/Heap] --> RV1["i32 value = 42"]
RP1 -.->|"Safe reference — Lifetime checked"| RV1
RM3[Borrow Checker] --> RC1["Prevents dangling refs at compile time"]
style RC1 fill:#51cf66,color:#000
style RP1 fill:#91e5a3,color:#000
Box<T> 堆分配可视化
#![allow(unused)]
fn main() {
fn box_allocation_example() {
// Stack allocation
let stack_value = 42;
// Heap allocation with Box
let heap_value = Box::new(42);
// Moving ownership
let moved_box = heap_value;
// heap_value is no longer accessible
}
}
graph TD
subgraph "Stack Frame"
SV["stack_value: 42"]
BP["heap_value: Box<i32>"]
BP2["moved_box: Box<i32>"]
end
subgraph "Heap"
HV["42"]
end
BP -->|"Owns"| HV
BP -.->|"Move ownership"| BP2
BP2 -->|"Now owns"| HV
subgraph "After Move"
BP_X["heap_value: [WARNING] MOVED"]
BP2_A["moved_box: Box<i32>"]
end
BP2_A -->|"Owns"| HV
style BP_X fill:#ff6b6b,color:#000
style HV fill:#91e5a3,color:#000
style BP2_A fill:#51cf66,color:#000
切片操作可视化
#![allow(unused)]
fn main() {
fn slice_operations() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
let full_slice = &data[..]; // [1,2,3,4,5,6,7,8]
let partial_slice = &data[2..6]; // [3,4,5,6]
let from_start = &data[..4]; // [1,2,3,4]
let to_end = &data[3..]; // [4,5,6,7,8]
}
}
graph TD
V["Vec: [1, 2, 3, 4, 5, 6, 7, 8]"]
V --> FS["&data[..] → all elements"]
V --> PS["&data[2..6] → [3, 4, 5, 6]"]
V --> SS["&data[..4] → [1, 2, 3, 4]"]
V --> ES["&data[3..] → [4, 5, 6, 7, 8]"]
style V fill:#e3f2fd,color:#000
style FS fill:#91e5a3,color:#000
style PS fill:#91e5a3,color:#000
style SS fill:#91e5a3,color:#000
style ES fill:#91e5a3,color:#000
Rust 的其他独特卖点与特性
- 线程间无数据竞争(编译期
Send/Sync检查) - 无移动后使用(不同于 C++
std::move留下僵尸对象) - 无未初始化变量
- 所有变量使用前必须初始化
- 无琐碎内存泄漏
DropTrait = 正确实现的 RAII,无需 Rule of Five- 编译器在变量离开作用域时自动释放内存
- 不会忘记解锁互斥锁
- 锁守卫是访问数据的唯一方式(
Mutex<T>包装数据,而非访问本身)
- 锁守卫是访问数据的唯一方式(
- 无异常处理复杂性
- 错误是值(
Result<T, E>),体现在函数签名中,用?传播
- 错误是值(
- 出色的类型推断、枚举、模式匹配、零成本抽象支持
- 内置依赖管理、构建、测试、格式化、lint 支持
cargo替代 make/CMake + lint + 测试框架
快速参考:Rust vs C/C++
| 概念 | C | C++ | Rust | 关键差异 |
|---|---|---|---|---|
| 内存管理 | malloc()/free() | unique_ptr, shared_ptr | Box<T>, Rc<T>, Arc<T> | 自动管理,无循环 |
| 数组 | int arr[10] | std::vector<T>, std::array<T> | Vec<T>, [T; N] | 默认边界检查 |
| 字符串 | char* 带 \0 | std::string, string_view | String, &str | 保证 UTF-8,生命周期检查 |
| 引用 | int* ptr | T&, T&&(移动) | &T, &mut T | 借用检查、生命周期 |
| 多态 | 函数指针 | 虚函数、继承 | Trait、trait 对象 | 组合优于继承 |
| 泛型编程 | 宏(void*) | 模板 | 泛型 + Trait 约束 | 更清晰的错误信息 |
| 错误处理 | 返回码、errno | 异常、std::optional | Result<T, E>, Option<T> | 无隐藏控制流 |
| NULL/空值安全 | ptr == NULL | nullptr, std::optional<T> | Option<T> | 强制空值检查 |
| 线程安全 | 手动(pthreads) | 手动同步 | 编译期保证 | 数据竞争不可能发生 |
| 构建系统 | Make、CMake | CMake、Make 等 | Cargo | 集成工具链 |
| 未定义行为 | 运行时崩溃 | 隐蔽 UB(有符号溢出、别名) | 编译期错误 | 安全有保证 |
为什么 C/C++ 开发者需要 Rust
你将学到:
- Rust 消除的完整问题清单——内存安全、未定义行为(Undefined Behavior)、数据竞争等
- 为什么
shared_ptr、unique_ptr等 C++ 缓解手段只是创可贴,而非根本解决方案- 在 safe Rust 中结构上不可能出现的具体 C/C++ 漏洞示例
想直接看代码? 跳转到 给我看代码
Rust 消除了什么——完整清单
在深入示例之前,先给出执行摘要。Safe Rust 从结构上防止 本清单中的每一个问题——不是靠纪律、工具或代码审查,而是靠类型系统和编译器:
| 被消除的问题 | C | C++ | Rust 如何防止 |
|---|---|---|---|
| 缓冲区溢出 / 下溢 | ✅ | ✅ | 所有数组、切片和字符串都携带边界信息;索引在运行时检查 |
| 内存泄漏(无需 GC) | ✅ | ✅ | Drop Trait(特征)= 正确实现的 RAII;自动清理,无需 Rule of Five |
| 悬垂指针 | ✅ | ✅ | 生命周期(Lifetime)系统在编译期证明引用比其指向对象活得更久 |
| 释放后使用(Use-after-free) | ✅ | ✅ | 所有权(Ownership)系统使其成为编译错误 |
| 移动后使用(Use-after-move) | — | ✅ | 移动是破坏性的——原绑定不再存在 |
| 未初始化变量 | ✅ | ✅ | 所有变量使用前必须初始化;编译器强制检查 |
| 整数溢出 / 下溢 UB | ✅ | ✅ | Debug 构建在溢出时 panic;Release 构建环绕(两种方式均为定义明确的行为) |
| NULL 指针解引用 / SEGV | ✅ | ✅ | 没有空指针;Option<T> 强制显式处理 |
| 数据竞争 | ✅ | ✅ | Send/Sync Trait + 借用检查器使数据竞争成为编译错误 |
| 不受控的副作用 | ✅ | ✅ | 默认不可变;修改需要显式 mut |
| 无继承(更好的可维护性) | — | ✅ | Trait + 组合替代类层次结构;促进复用而不产生耦合 |
| 无异常;可预测的控制流 | — | ✅ | 错误是值(Result<T, E>);无法忽略,没有隐藏的 throw 路径 |
| 迭代器失效 | — | ✅ | 借用检查器禁止在迭代时修改集合 |
| 引用循环 / 终结器泄漏 | — | ✅ | 所有权呈树形结构;Rc 循环是可选的,可用 Weak 检测 |
| 不会忘记解锁互斥锁 | ✅ | ✅ | Mutex<T> 包装数据;锁守卫是访问数据的唯一方式 |
| 未定义行为(一般情况) | ✅ | ✅ | Safe Rust 零未定义行为;unsafe 代码块显式且可审计 |
结论: 这些不是靠编码规范追求的理想目标。它们是编译期保证。只要代码能编译通过,这些 bug 就不可能存在。
C 与 C++ 共同面临的问题
想跳过示例? 跳转到 Rust 如何解决这一切,或直接前往 给我看代码
两种语言共享一组核心内存安全问题,它们是超过 70% CVE(Common Vulnerabilities and Exposures,常见漏洞与披露)的根本原因:
缓冲区溢出
C 的数组、指针和字符串没有内在边界。越界极其容易:
#include <stdlib.h>
#include <string.h>
void buffer_dangers() {
char buffer[10];
strcpy(buffer, "This string is way too long!"); // Buffer overflow
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // Loses size information
ptr[10] = 42; // No bounds check — undefined behavior
}
在 C++ 中,std::vector::operator[] 仍不进行边界检查。只有 .at() 会检查——但谁会去捕获那个异常?
悬垂指针与释放后使用
int *bar() {
int i = 42;
return &i; // Returns address of stack variable — dangling!
}
void use_after_free() {
char *p = (char *)malloc(20);
free(p);
*p = '\0'; // Use after free — undefined behavior
}
未初始化变量与未定义行为
C 和 C++ 都允许未初始化变量。产生的值是不确定的,读取它们是未定义行为:
int x; // Uninitialized
if (x > 0) { ... } // UB — x could be anything
对于无符号类型,C 中整数溢出是定义明确的;对于有符号类型则是未定义的。在 C++ 中,有符号溢出同样是未定义行为。两种编译器都会利用这一点做“优化“,以出人意料的方式破坏程序。
NULL 指针解引用
int *ptr = NULL;
*ptr = 42; // SEGV — but the compiler won't stop you
在 C++ 中,std::optional<T> 有帮助,但写法冗长,且常被 .value() 绕过——后者会抛出异常。
可视化:共同问题
graph TD
ROOT["C/C++ Memory Safety Issues"] --> BUF["Buffer Overflows"]
ROOT --> DANGLE["Dangling Pointers"]
ROOT --> UAF["Use-After-Free"]
ROOT --> UNINIT["Uninitialized Variables"]
ROOT --> NULL["NULL Dereferences"]
ROOT --> UB["Undefined Behavior"]
ROOT --> RACE["Data Races"]
BUF --> BUF1["No bounds on arrays/pointers"]
DANGLE --> DANGLE1["Returning stack addresses"]
UAF --> UAF1["Reusing freed memory"]
UNINIT --> UNINIT1["Indeterminate values"]
NULL --> NULL1["No forced null checks"]
UB --> UB1["Signed overflow, aliasing"]
RACE --> RACE1["No compile-time safety"]
style ROOT fill:#ff6b6b,color:#000
style BUF fill:#ffa07a,color:#000
style DANGLE fill:#ffa07a,color:#000
style UAF fill:#ffa07a,color:#000
style UNINIT fill:#ffa07a,color:#000
style NULL fill:#ffa07a,color:#000
style UB fill:#ffa07a,color:#000
style RACE fill:#ffa07a,color:#000
C++ 在此基础上又增加了更多问题
C 读者:如果不使用 C++,可以跳到 Rust 如何解决这些问题。
想直接看代码? 跳转到 给我看代码
C++ 引入了智能指针、RAII、移动语义和异常来解决 C 的问题。但这些只是创可贴,不是根治——它们把失败模式从“运行时崩溃“变成了“运行时更隐蔽的 bug“:
unique_ptr 与 shared_ptr——创可贴,而非解决方案
C++ 智能指针相比原始 malloc/free 是显著改进,但并未解决底层问题:
| C++ 缓解手段 | 解决了什么 | 没解决什么 |
|---|---|---|
std::unique_ptr | 通过 RAII 防止泄漏 | 移动后使用仍能编译通过;留下僵尸 nullptr |
std::shared_ptr | 共享所有权 | 引用循环会静默泄漏;weak_ptr 纪律需手动维护 |
std::optional | 替代部分空值用法 | .value() 在为空时抛出异常——隐藏的控制流 |
std::string_view | 避免拷贝 | 若源字符串被释放则悬垂——无生命周期检查 |
| 移动语义 | 高效转移 | 被移动的对象处于**“有效但未指定状态”**——随时可能 UB |
| RAII | 自动清理 | 需要正确实现 Rule of Five;一处失误全盘皆输 |
// unique_ptr: use-after-move compiles cleanly
std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = std::move(ptr);
std::cout << *ptr; // Compiles! Undefined behavior at runtime.
// In Rust, this is a compile error: "value used after move"
// shared_ptr: reference cycles leak silently
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> parent; // Cycle! Destructor never called.
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->parent = a; // Memory leak — ref count never reaches 0
// In Rust, Rc<T> + Weak<T> makes cycles explicit and breakable
移动后使用——无声的杀手
C++ 的 std::move 不是移动——它是类型转换。原对象仍处于“有效但未指定状态“。编译器允许你继续使用它:
auto vec = std::make_unique<std::vector<int>>({1, 2, 3});
auto vec2 = std::move(vec);
vec->size(); // Compiles! But dereferencing nullptr — crash at runtime
在 Rust 中,移动是破坏性的。原绑定已不存在:
#![allow(unused)]
fn main() {
let vec = vec![1, 2, 3];
let vec2 = vec; // Move — vec is consumed
// vec.len(); // Compile error: value used after move
}
迭代器失效——来自生产环境 C++ 的真实 bug
这些不是牵强附会的例子——它们代表在大型 C++ 代码库中发现的真实 bug 模式:
// BUG 1: erase without reassigning iterator (undefined behavior)
while (it != pending_faults.end()) {
if (*it != nullptr && (*it)->GetId() == fault->GetId()) {
pending_faults.erase(it); // ← iterator invalidated!
removed_count++; // next loop uses dangling iterator
} else {
++it;
}
}
// Fix: it = pending_faults.erase(it);
// BUG 2: index-based erase skips elements
for (auto i = 0; i < entries.size(); i++) {
if (config_status == ConfigDisable::Status::Disabled) {
entries.erase(entries.begin() + i); // ← shifts elements
} // i++ skips the shifted one
}
// BUG 3: one erase path correct, the other isn't
while (it != incomplete_ids.end()) {
if (current_action == nullptr) {
incomplete_ids.erase(it); // ← BUG: iterator not reassigned
continue;
}
it = incomplete_ids.erase(it); // ← Correct path
}
这些代码编译时没有任何警告。 在 Rust 中,借用检查器使以上三种情况都成为编译错误——你无法在迭代集合的同时修改它,绝无例外。
异常安全与 dynamic_cast/new 模式
现代 C++ 代码库仍大量依赖没有编译期安全的模式:
// Typical C++ factory pattern — every branch is a potential bug
DriverBase* driver = nullptr;
if (dynamic_cast<ModelA*>(device)) {
driver = new DriverForModelA(framework);
} else if (dynamic_cast<ModelB*>(device)) {
driver = new DriverForModelB(framework);
}
// What if driver is still nullptr? What if new throws? Who owns driver?
在一个典型的 10 万行 C++ 代码库中,你可能发现数百次 dynamic_cast 调用(每次都是潜在的运行时失败)、数百次原始 new 调用(每次都是潜在的泄漏),以及数百个 virtual/override 方法(到处都有 vtable 开销)。
悬垂引用与 lambda 捕获
int& get_reference() {
int x = 42;
return x; // Dangling reference — compiles, UB at runtime
}
auto make_closure() {
int local = 42;
return [&local]() { return local; }; // Dangling capture!
}
可视化:C++ 额外问题
graph TD
ROOT["C++ Additional Problems<br/>(on top of C issues)"] --> UAM["Use-After-Move"]
ROOT --> CYCLE["Reference Cycles"]
ROOT --> ITER["Iterator Invalidation"]
ROOT --> EXC["Exception Safety"]
ROOT --> TMPL["Template Error Messages"]
UAM --> UAM1["std::move leaves zombie<br/>Compiles without warning"]
CYCLE --> CYCLE1["shared_ptr cycles leak<br/>Destructor never called"]
ITER --> ITER1["erase() invalidates iterators<br/>Real production bugs"]
EXC --> EXC1["Partial construction<br/>new without try/catch"]
TMPL --> TMPL1["30+ lines of nested<br/>template instantiation errors"]
style ROOT fill:#ff6b6b,color:#000
style UAM fill:#ffa07a,color:#000
style CYCLE fill:#ffa07a,color:#000
style ITER fill:#ffa07a,color:#000
style EXC fill:#ffa07a,color:#000
style TMPL fill:#ffa07a,color:#000
Rust 如何解决这一切
上文列出的每一个问题——无论来自 C 还是 C++——都被 Rust 的编译期保证所防止:
| 问题 | Rust 的解决方案 |
|---|---|
| 缓冲区溢出 | 切片携带长度;索引进行边界检查 |
| 悬垂指针 / 释放后使用 | 生命周期系统在编译期证明引用有效 |
| 移动后使用 | 移动是破坏性的——编译器拒绝让你再触碰原值 |
| 内存泄漏 | Drop Trait = 无需 Rule of Five 的 RAII;自动、正确的清理 |
| 引用循环 | 所有权呈树形结构;Rc + Weak 使循环显式化 |
| 迭代器失效 | 借用检查器禁止在借用集合时修改它 |
| NULL 指针 | 没有 null。Option<T> 通过模式匹配强制显式处理 |
| 数据竞争 | Send/Sync Trait 使数据竞争成为编译错误 |
| 未初始化变量 | 所有变量必须初始化;编译器强制检查 |
| 整数 UB | Debug 在溢出时 panic;Release 环绕(均为定义明确的行为) |
| 异常 | 没有异常;Result<T, E> 在类型签名中可见,用 ? 传播 |
| 继承复杂性 | Trait + 组合;没有菱形问题(Diamond Problem),没有 vtable 脆弱性 |
| 忘记解锁互斥锁 | Mutex<T> 包装数据;锁守卫是唯一的访问路径 |
#![allow(unused)]
fn main() {
fn rust_prevents_everything() {
// ✅ No buffer overflow — bounds checked
let arr = [1, 2, 3, 4, 5];
// arr[10]; // panic at runtime, never UB
// ✅ No use-after-move — compile error
let data = vec![1, 2, 3];
let moved = data;
// data.len(); // error: value used after move
// ✅ No dangling pointer — lifetime error
// let r;
// { let x = 5; r = &x; } // error: x does not live long enough
// ✅ No null — Option forces handling
let maybe: Option<i32> = None;
// maybe.unwrap(); // panic, but you'd use match or if let instead
// ✅ No data race — compile error
// let mut shared = vec![1, 2, 3];
// std::thread::spawn(|| shared.push(4)); // error: closure may outlive
// shared.push(5); // borrowed value
}
}
Rust 的安全模型——全貌
graph TD
RUST["Rust Safety Guarantees"] --> OWN["Ownership System"]
RUST --> BORROW["Borrow Checker"]
RUST --> TYPES["Type System"]
RUST --> TRAITS["Send/Sync Traits"]
OWN --> OWN1["No use-after-free<br/>No use-after-move<br/>No double-free"]
BORROW --> BORROW1["No dangling references<br/>No iterator invalidation<br/>No data races through refs"]
TYPES --> TYPES1["No NULL (Option<T>)<br/>No exceptions (Result<T,E>)<br/>No uninitialized values"]
TRAITS --> TRAITS1["No data races<br/>Send = safe to transfer<br/>Sync = safe to share"]
style RUST fill:#51cf66,color:#000
style OWN fill:#91e5a3,color:#000
style BORROW fill:#91e5a3,color:#000
style TYPES fill:#91e5a3,color:#000
style TRAITS fill:#91e5a3,color:#000
快速参考:C vs C++ vs Rust
| 概念 | C | C++ | Rust | 关键差异 |
|---|---|---|---|---|
| 内存管理 | malloc()/free() | unique_ptr, shared_ptr | Box<T>, Rc<T>, Arc<T> | 自动管理,无循环,无僵尸对象 |
| 数组 | int arr[10] | std::vector<T>, std::array<T> | Vec<T>, [T; N] | 默认进行边界检查 |
| 字符串 | char* 带 \0 | std::string, string_view | String, &str | 保证 UTF-8,生命周期检查 |
| 引用 | int*(原始指针) | T&, T&&(移动) | &T, &mut T | 生命周期 + 借用检查 |
| 多态 | 函数指针 | 虚函数、继承 | Trait、trait 对象 | 组合优于继承 |
| 泛型 | 宏 / void* | 模板 | 泛型 + Trait 约束 | 清晰的错误信息 |
| 错误处理 | 返回码、errno | 异常、std::optional | Result<T, E>, Option<T> | 无隐藏控制流 |
| NULL 安全 | ptr == NULL | nullptr, std::optional<T> | Option<T> | 强制空值检查 |
| 线程安全 | 手动(pthreads) | 手动(std::mutex 等) | 编译期 Send/Sync | 数据竞争不可能发生 |
| 构建系统 | Make、CMake | CMake、Make 等 | Cargo | 集成工具链 |
| 未定义行为 | 泛滥 | 隐蔽(有符号溢出、别名) | Safe 代码中为零 | 安全有保证 |
少说多做:给我看代码
你将学到: 你的第一个 Rust 程序——
fn main()、println!(),以及 Rust 宏与 C/C++ 预处理器宏的根本差异。学完后你将能编写、编译并运行简单的 Rust 程序。
fn main() {
println!("Hello world from Rust");
}
- 上述语法对熟悉 C 风格语言的人应该很亲切
- Rust 中所有函数以
fn关键字开头 - 可执行文件的默认入口点是
main() println!看起来像函数,实际上是宏。Rust 宏与 C/C++ 预处理器宏截然不同——它们是卫生的、类型安全的,操作语法树而非文本替换
- Rust 中所有函数以
- 两种快速尝试 Rust 片段的方式:
- 在线:Rust Playground——粘贴代码、点击 Run、分享结果。无需安装
- 本地 REPL:安装
evcxr_repl获得交互式 Rust REPL(类似 Python REPL,但用于 Rust):
cargo install --locked evcxr_repl
evcxr # Start the REPL, type Rust expressions interactively
Rust 本地安装
- 可通过以下方式本地安装 Rust
- Windows:https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe
- Linux / WSL:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- Rust 生态由以下组件构成
rustc是独立编译器,但很少直接使用- 首选工具
cargo是瑞士军刀,用于依赖管理、构建、测试、格式化、lint 等 - Rust 工具链有
stable、beta和nightly(实验性)通道,我们使用stable。用rustup update升级每六周发布的stable安装
- 我们还会安装 VSCode 的
rust-analyzer插件
Rust 包(crate)
- Rust 二进制文件通过包(下文称 crate)创建
- crate 可独立存在,也可依赖其他 crate。依赖 crate 可以是本地或远程。第三方 crate 通常从名为
crates.io的中央仓库下载。 cargo工具自动处理 crate 及其依赖的下载。概念上等同于链接 C 库- crate 依赖在名为
Cargo.toml的文件中声明。它还定义 crate 的目标类型:独立可执行文件、静态库、动态库(较少见) - 参考:https://doc.rust-lang.org/cargo/reference/cargo-targets.html
- crate 可独立存在,也可依赖其他 crate。依赖 crate 可以是本地或远程。第三方 crate 通常从名为
Cargo 与传统 C 构建系统对比
依赖管理对比
graph TD
subgraph "Traditional C Build Process"
CC["C Source Files<br/>(.c, .h)"]
CM["Manual Makefile<br/>or CMake"]
CL["Linker"]
CB["Final Binary"]
CC --> CM
CM --> CL
CL --> CB
CDep["Manual dependency<br/>management"]
CLib1["libcurl-dev<br/>(apt install)"]
CLib2["libjson-dev<br/>(apt install)"]
CInc["Manual include paths<br/>-I/usr/include/curl"]
CLink["Manual linking<br/>-lcurl -ljson"]
CDep --> CLib1
CDep --> CLib2
CLib1 --> CInc
CLib2 --> CInc
CInc --> CM
CLink --> CL
C_ISSUES["[ERROR] Version conflicts<br/>[ERROR] Platform differences<br/>[ERROR] Missing dependencies<br/>[ERROR] Linking order matters<br/>[ERROR] No automated updates"]
end
subgraph "Rust Cargo Build Process"
RS["Rust Source Files<br/>(.rs)"]
CT["Cargo.toml<br/>[dependencies]<br/>reqwest = '0.11'<br/>serde_json = '1.0'"]
CRG["Cargo Build System"]
RB["Final Binary"]
RS --> CRG
CT --> CRG
CRG --> RB
CRATES["crates.io<br/>(Package registry)"]
DEPS["Automatic dependency<br/>resolution"]
LOCK["Cargo.lock<br/>(Version pinning)"]
CRATES --> DEPS
DEPS --> CRG
CRG --> LOCK
R_BENEFITS["[OK] Semantic versioning<br/>[OK] Automatic downloads<br/>[OK] Cross-platform<br/>[OK] Transitive dependencies<br/>[OK] Reproducible builds"]
end
style C_ISSUES fill:#ff6b6b,color:#000
style R_BENEFITS fill:#91e5a3,color:#000
style CM fill:#ffa07a,color:#000
style CDep fill:#ffa07a,color:#000
style CT fill:#91e5a3,color:#000
style CRG fill:#91e5a3,color:#000
style DEPS fill:#91e5a3,color:#000
style CRATES fill:#91e5a3,color:#000
Cargo 项目结构
my_project/
|-- Cargo.toml # Project configuration (like package.json)
|-- Cargo.lock # Exact dependency versions (auto-generated)
|-- src/
| |-- main.rs # Main entry point for binary
| |-- lib.rs # Library root (if creating a library)
| `-- bin/ # Additional binary targets
|-- tests/ # Integration tests
|-- examples/ # Example code
|-- benches/ # Benchmarks
`-- target/ # Build artifacts (like C's build/ or obj/)
|-- debug/ # Debug builds (fast compile, slow runtime)
`-- release/ # Release builds (slow compile, fast runtime)
常用 Cargo 命令
graph LR
subgraph "Project Lifecycle"
NEW["cargo new my_project<br/>[FOLDER] Create new project"]
CHECK["cargo check<br/>[SEARCH] Fast syntax check"]
BUILD["cargo build<br/>[BUILD] Compile project"]
RUN["cargo run<br/>[PLAY] Build and execute"]
TEST["cargo test<br/>[TEST] Run all tests"]
NEW --> CHECK
CHECK --> BUILD
BUILD --> RUN
BUILD --> TEST
end
subgraph "Advanced Commands"
UPDATE["cargo update<br/>[CHART] Update dependencies"]
FORMAT["cargo fmt<br/>[SPARKLES] Format code"]
LINT["cargo clippy<br/>[WRENCH] Lint and suggestions"]
DOC["cargo doc<br/>[BOOKS] Generate documentation"]
PUBLISH["cargo publish<br/>[PACKAGE] Publish to crates.io"]
end
subgraph "Build Profiles"
DEBUG["cargo build<br/>(debug profile)<br/>Fast compile<br/>Slow runtime<br/>Debug symbols"]
RELEASE["cargo build --release<br/>(release profile)<br/>Slow compile<br/>Fast runtime<br/>Optimized"]
end
style NEW fill:#a3d5ff,color:#000
style CHECK fill:#91e5a3,color:#000
style BUILD fill:#ffa07a,color:#000
style RUN fill:#ffcc5c,color:#000
style TEST fill:#c084fc,color:#000
style DEBUG fill:#94a3b8,color:#000
style RELEASE fill:#ef4444,color:#000
示例:cargo 与 crate
- 本示例是一个无其他依赖的独立可执行 crate
- 使用以下命令创建名为
helloworld的新 crate
cargo new helloworld
cd helloworld
cat Cargo.toml
- 默认
cargo run会编译并运行 crate 的debug(未优化)版本。要运行release版本,使用cargo run --release - 实际二进制文件位于
target文件夹下的debug或release子目录 - 你可能还注意到源码同目录下有
Cargo.lock文件。它自动生成,不应手动修改- 我们稍后会再讨论
Cargo.lock的具体用途
- 我们稍后会再讨论
Rust 内置类型
你将学到: Rust 的基本类型(
i32、u64、f64、bool、char)、类型推断、显式类型标注,以及它们与 C/C++ 基本类型的对比。无隐式转换——Rust 要求显式类型转换。
- Rust 支持类型推断,也允许显式指定类型
| 说明 | 类型 | 示例 |
|---|---|---|
| 有符号整数 | i8, i16, i32, i64, i128, isize | -1, 42, 1_00_000, 1_00_000i64 |
| 无符号整数 | u8, u16, u32, u64, u128, usize | 0, 42, 42u32, 42u64 |
| 浮点数 | f32, f64 | 0.0, 0.42 |
| Unicode | char | ‘a’, ‘$’ |
| 布尔值 | bool | true, false |
- Rust 允许在数字中任意使用
_以提高可读性
Rust 类型指定与赋值
- Rust 使用
let关键字为变量赋值。变量类型可在:后可选指定
fn main() {
let x : i32 = 42;
// These two assignments are logically equivalent
let y : u32 = 42;
let z = 42u32;
}
- 函数参数和返回值(如有)需要显式类型。以下函数接受 u8 参数并返回 u32
#![allow(unused)]
fn main() {
fn foo(x : u8) -> u32
{
return x as u32 * x as u32;
}
}
- 未使用的变量以
_为前缀以避免编译器警告
Rust 类型指定与推断
- Rust 可根据上下文自动推断变量类型。
- ▶ 在 Rust Playground 中尝试
fn secret_of_life_u32(x : u32) {
println!("The u32 secret_of_life is {}", x);
}
fn secret_of_life_u8(x : u8) {
println!("The u8 secret_of_life is {}", x);
}
fn main() {
let a = 42; // The let keyword assigns a value; type of a is u32
let b = 42; // The let keyword assigns a value; inferred type of b is u8
secret_of_life_u32(a);
secret_of_life_u8(b);
}
Rust 变量与可变性
- Rust 变量默认不可变,除非使用
mut关键字声明可变。例如,以下代码除非将let a = 42改为let mut a = 42,否则无法编译
fn main() {
let a = 42; // Must be changed to let mut a = 42 to permit the assignment below
a = 43; // Will not compile unless the above is changed
}
- Rust 允许重用变量名(遮蔽,shadowing)
fn main() {
let a = 42;
{
let a = 43; //OK: Different variable with the same name
}
// a = 43; // Not permitted
let a = 43; // Ok: New variable and assignment
}
Rust if 关键字
你将学到: Rust 的控制流结构——作为表达式的
if/else、loop/while/for、match,以及它们与 C/C++ 对应物的差异。关键洞察:大多数 Rust 控制流会返回值。
- 在 Rust 中,
if实际上是表达式,即可用于赋值,也表现得像语句。▶ 试一试
fn main() {
let x = 42;
if x < 42 {
println!("Smaller than the secret of life");
} else if x == 42 {
println!("Is equal to the secret of life");
} else {
println!("Larger than the secret of life");
}
let is_secret_of_life = if x == 42 {true} else {false};
println!("{}", is_secret_of_life);
}
Rust 使用 while 和 for 的循环
while关键字可在表达式为真时循环
fn main() {
let mut x = 40;
while x != 42 {
x += 1;
}
}
for关键字可遍历范围
fn main() {
// Will not print 43; use 40..=43 to include last element
for x in 40..43 {
println!("{}", x);
}
}
Rust 使用 loop 的循环
loop关键字创建无限循环,直到遇到break
fn main() {
let mut x = 40;
// Change the below to 'here: loop to specify optional label for the loop
loop {
if x == 42 {
break; // Use break x; to return the value of x
}
x += 1;
}
}
break语句可包含可选表达式,用于为loop表达式赋值continue关键字可返回loop顶部- 循环标签可与
break或continue配合使用,在处理嵌套循环时很有用
Rust 表达式块
- Rust 表达式块是放在
{}中的一串表达式。求值结果为块中最后一个表达式
fn main() {
let x = {
let y = 40;
y + 2 // Note: ; must be omitted
};
// Notice the Python style printing
println!("{x}");
}
- Rust 风格是用此省略函数中的
return关键字
fn is_secret_of_life(x: u32) -> bool {
// Same as if x == 42 {true} else {false}
x == 42 // Note: ; must be omitted
}
fn main() {
println!("{}", is_secret_of_life(42));
}
5. 数据结构
Rust 数组类型
你将学到: Rust 的核心数据结构——数组、元组、切片、字符串、结构体、
Vec和HashMap。本章内容较密集;重点理解String与&str的区别,以及结构体的工作方式。引用与借用将在第 7 章深入讲解。
- 数组包含固定数量的同类型元素
- 与 Rust 中其他类型一样,数组默认不可变(除非使用
mut) - 数组使用
[]索引,并会进行边界检查。可用len()方法获取数组长度
- 与 Rust 中其他类型一样,数组默认不可变(除非使用
fn get_index(y : usize) -> usize {
y+1
}
fn main() {
// Initializes an array of 3 elements and sets all to 42
let a : [u8; 3] = [42; 3];
// Alternative syntax
// let a = [42u8, 42u8, 42u8];
for x in a {
println!("{x}");
}
let y = get_index(a.len());
// Commenting out the below will cause a panic
//println!("{}", a[y]);
}
Rust 数组类型(续)
- 数组可以嵌套
- Rust 内置多种打印格式化器。下面示例中,
:?是debug打印格式化器。:#?可用于pretty print。这些格式化器可按类型自定义(后续会介绍)
- Rust 内置多种打印格式化器。下面示例中,
fn main() {
let a = [
[40, 0], // Define a nested array
[41, 0],
[42, 1],
];
for x in a {
println!("{x:?}");
}
}
Rust 元组
- 元组大小固定,可将任意类型组合成单一复合类型
- 各成员类型可通过相对位置索引(
.0、.1、.2、……)。空元组()称为 unit 值,相当于 void 返回值 - Rust 支持元组解构,便于将变量绑定到各个元素
- 各成员类型可通过相对位置索引(
fn get_tuple() -> (u32, bool) {
(42, true)
}
fn main() {
let t : (u8, bool) = (42, true);
let u : (u32, bool) = (43, false);
println!("{}, {}", t.0, t.1);
println!("{}, {}", u.0, u.1);
let (num, flag) = get_tuple(); // Tuple destructuring
println!("{num}, {flag}");
}
Rust 引用
- Rust 中的引用大致相当于 C 中的指针,但有关键差异
- 在任意时刻,对同一变量可以有任意数量的只读(不可变)引用。引用不能超出变量作用域(这是称为生命周期的核心概念;后续详述)
- 对可变变量只允许一个可写(可变)引用,且不得与其他任何引用重叠。
fn main() {
let mut a = 42;
{
let b = &a;
let c = b;
println!("{} {}", *b, *c); // The compiler automatically dereferences *c
let d = &mut a;
/*
* Uncommenting the line below would cause the
* program to not compile, because `b` is used
* while the mutable reference `d` is live in the current scope
*
* You cannot have a mutable and immutable reference in use in the same scope
* at the same time!
*/
// println!("{}", *b);
}
let d = &mut a; // Ok: b and c are not in scope
*d = 43;
}
Rust 切片
- Rust 引用可用于创建数组的子集
- 与编译期长度固定的数组不同,切片可以是任意大小。内部实现上,切片是包含长度与指向原数组首元素指针的「胖指针」
fn main() {
let a = [40, 41, 42, 43];
let b = &a[1..a.len()]; // A slice starting with the second element in the original
let c = &a[1..]; // Same as the above
let d = &a[..]; // Same as &a[0..] or &a[0..a.len()]
println!("{b:?} {c:?} {d:?}");
}
Rust 常量与 static
const关键字用于定义常量。常量在编译期求值并内联到程序中static关键字用于定义类似 C/C++ 全局变量的等价物。static 变量有可寻址内存位置,创建一次并在程序整个生命周期内存在
const SECRET_OF_LIFE: u32 = 42;
static GLOBAL_VARIABLE : u32 = 2;
fn main() {
println!("The secret of life is {}", SECRET_OF_LIFE);
println!("Value of global variable is {GLOBAL_VARIABLE}")
}
Rust 字符串:String 与 &str
- Rust 有两种用途不同的字符串类型
String— 拥有所有权、堆分配、可增长(类似 C 的malloc缓冲区,或 C++ 的std::string)&str— 借用、轻量引用(类似带长度的 Cconst char*,或 C++ 的std::string_view——但&str经生命周期检查,不会悬垂)- 与 C 以 null 结尾的字符串不同,Rust 字符串跟踪长度并保证为有效 UTF-8
面向 C++ 开发者:
String≈std::string,&str≈std::string_view。与std::string_view不同,&str在其整个生命周期内由借用检查器保证有效。
String 与 &str:拥有 vs 借用
生产实践: 参见 JSON 处理:nlohmann::json → serde,了解生产代码中 serde 与字符串处理的配合。
| 方面 | C char* | C++ std::string | Rust String | Rust &str |
|---|---|---|---|---|
| 内存 | 手动(malloc/free) | 堆分配,拥有缓冲区 | 堆分配,自动释放 | 借用引用(经生命周期检查) |
| 可变性 | 指针始终可变 | 可变 | 需 mut 才可变 | 始终不可变 |
| 大小信息 | 无(依赖 '\0') | 跟踪长度与容量 | 跟踪长度与容量 | 跟踪长度(胖指针) |
| 编码 | 未指定(通常 ASCII) | 未指定(通常 ASCII) | 保证有效 UTF-8 | 保证有效 UTF-8 |
| Null 终止符 | 需要 | 需要(c_str()) | 不使用 | 不使用 |
fn main() {
// &str - string slice (borrowed, immutable, usually a string literal)
let greeting: &str = "Hello"; // Points to read-only memory
// String - owned, heap-allocated, growable
let mut owned = String::from(greeting); // Copies data to heap
owned.push_str(", World!"); // Grow the string
owned.push('!'); // Append a single character
// Converting between String and &str
let slice: &str = &owned; // String -> &str (free, just a borrow)
let owned2: String = slice.to_string(); // &str -> String (allocates)
let owned3: String = String::from(slice); // Same as above
// String concatenation (note: + consumes the left operand)
let hello = String::from("Hello");
let world = String::from(", World!");
let combined = hello + &world; // hello is moved (consumed), world is borrowed
// println!("{hello}"); // Won't compile: hello was moved
// Use format! to avoid move issues
let a = String::from("Hello");
let b = String::from("World");
let combined = format!("{a}, {b}!"); // Neither a nor b is consumed
println!("{combined}");
}
为何不能用 [] 索引字符串
fn main() {
let s = String::from("hello");
// let c = s[0]; // Won't compile! Rust strings are UTF-8, not byte arrays
// Safe alternatives:
let first_char = s.chars().next(); // Option<char>: Some('h')
let as_bytes = s.as_bytes(); // &[u8]: raw UTF-8 bytes
let substring = &s[0..1]; // &str: "h" (byte range, must be valid UTF-8 boundary)
println!("First char: {:?}", first_char);
println!("Bytes: {:?}", &as_bytes[..5]);
}
练习:字符串操作
🟢 入门
- 编写函数
fn count_words(text: &str) -> usize,统计字符串中由空白分隔的单词数 - 编写函数
fn longest_word(text: &str) -> &str,返回最长单词(提示:需考虑生命周期——为何返回类型是&str而非String?)
Solution (click to expand)
fn count_words(text: &str) -> usize {
text.split_whitespace().count()
}
fn longest_word(text: &str) -> &str {
text.split_whitespace()
.max_by_key(|word| word.len())
.unwrap_or("")
}
fn main() {
let text = "the quick brown fox jumps over the lazy dog";
println!("Word count: {}", count_words(text)); // 9
println!("Longest word: {}", longest_word(text)); // "jumps"
}
Rust 结构体
struct关键字声明用户定义的结构体类型struct成员可以具名,也可以匿名(元组结构体)
- 与 C++ 等语言不同,Rust 没有「数据继承」概念
fn main() {
struct MyStruct {
num: u32,
is_secret_of_life: bool,
}
let x = MyStruct {
num: 42,
is_secret_of_life: true,
};
let y = MyStruct {
num: x.num,
is_secret_of_life: x.is_secret_of_life,
};
let z = MyStruct { num: x.num, ..x }; // The .. means copy remaining
println!("{} {} {}", x.num, y.is_secret_of_life, z.num);
}
Rust 元组结构体
- Rust 元组结构体类似元组,各字段没有名称
- 与元组一样,用
.0、.1、.2、…… 访问各元素。常见用途是用元组结构体包装基本类型以创建自定义类型。这有助于避免混淆同类型的不同值
- 与元组一样,用
struct WeightInGrams(u32);
struct WeightInMilligrams(u32);
fn to_weight_in_grams(kilograms: u32) -> WeightInGrams {
WeightInGrams(kilograms * 1000)
}
fn to_weight_in_milligrams(w : WeightInGrams) -> WeightInMilligrams {
WeightInMilligrams(w.0 * 1000)
}
fn main() {
let x = to_weight_in_grams(42);
let y = to_weight_in_milligrams(x);
// let z : WeightInGrams = x; // Won't compile: x was moved into to_weight_in_milligrams()
// let a : WeightInGrams = y; // Won't compile: type mismatch (WeightInMilligrams vs WeightInGrams)
}
注意:#[derive(...)] 属性会为结构体和枚举自动生成常见 Trait 实现。本课程中会频繁使用:
#[derive(Debug, Clone, PartialEq)]
struct Point { x: i32, y: i32 }
fn main() {
let p = Point { x: 1, y: 2 };
println!("{:?}", p); // Debug: works because of #[derive(Debug)]
let p2 = p.clone(); // Clone: works because of #[derive(Clone)]
assert_eq!(p, p2); // PartialEq: works because of #[derive(PartialEq)]
}
Trait 系统后续会深入讲解,但 #[derive(Debug)] 非常实用,几乎应对每个 struct 和 enum 都加上。
Rust Vec 类型
Vec<T>类型实现动态堆分配缓冲区(类似 C 中手动管理的malloc/realloc数组,或 C++ 的std::vector)- 与固定大小数组不同,
Vec可在运行时增长与收缩 Vec拥有其数据并自动管理内存分配/释放
- 与固定大小数组不同,
- 常见操作:
push()、pop()、insert()、remove()、len()、capacity()
fn main() {
let mut v = Vec::new(); // Empty vector, type inferred from usage
v.push(42); // Add element to end - Vec<i32>
v.push(43);
// Safe iteration (preferred)
for x in &v { // Borrow elements, don't consume vector
println!("{x}");
}
// Initialization shortcuts
let mut v2 = vec![1, 2, 3, 4, 5]; // Macro for initialization
let v3 = vec![0; 10]; // 10 zeros
// Safe access methods (preferred over indexing)
match v2.get(0) {
Some(first) => println!("First: {first}"),
None => println!("Empty vector"),
}
// Useful methods
println!("Length: {}, Capacity: {}", v2.len(), v2.capacity());
if let Some(last) = v2.pop() { // Remove and return last element
println!("Popped: {last}");
}
// Dangerous: direct indexing (can panic!)
// println!("{}", v2[100]); // Would panic at runtime
}
生产实践: 参见 避免未检查索引,了解生产 Rust 代码中安全的
.get()模式。
Rust HashMap 类型
HashMap实现泛型key->value查找(亦称dictionary或map)
fn main() {
use std::collections::HashMap; // Need explicit import, unlike Vec
let mut map = HashMap::new(); // Allocate an empty HashMap
map.insert(40, false); // Type is inferred as int -> bool
map.insert(41, false);
map.insert(42, true);
for (key, value) in map {
println!("{key} {value}");
}
let map = HashMap::from([(40, false), (41, false), (42, true)]);
if let Some(x) = map.get(&43) {
println!("43 was mapped to {x:?}");
} else {
println!("No mapping was found for 43");
}
let x = map.get(&43).or(Some(&false)); // Default value if key isn't found
println!("{x:?}");
}
练习:Vec 与 HashMap
🟢 入门
- 创建包含若干条目的
HashMap<u32, bool>(确保部分值为true、部分为false)。遍历 hashmap 中所有元素,将键放入一个Vec,值放入另一个
Solution (click to expand)
use std::collections::HashMap;
fn main() {
let map = HashMap::from([(1, true), (2, false), (3, true), (4, false)]);
let mut keys = Vec::new();
let mut values = Vec::new();
for (k, v) in &map {
keys.push(*k);
values.push(*v);
}
println!("Keys: {keys:?}");
println!("Values: {values:?}");
// Alternative: use iterators with unzip()
let (keys2, values2): (Vec<u32>, Vec<bool>) = map.into_iter().unzip();
println!("Keys (unzip): {keys2:?}");
println!("Values (unzip): {values2:?}");
}
深入:C++ 引用 vs Rust 引用
面向 C++ 开发者: C++ 程序员常假设 Rust 的
&T与 C++ 的T&类似。表面相似,但存在根本差异,容易混淆。C 开发者可跳过本节——Rust 引用在 所有权与借用 中已有讲解。
1. 无右值引用与万能引用
在 C++ 中,&& 依上下文有两种含义:
// C++: && means different things:
int&& rref = 42; // Rvalue reference — binds to temporaries
void process(Widget&& w); // Rvalue reference — caller must std::move
// Universal (forwarding) reference — deduced template context:
template<typename T>
void forward(T&& arg) { // NOT an rvalue ref! Deduced as T& or T&&
inner(std::forward<T>(arg)); // Perfect forwarding
}
Rust 中不存在这些。 && 只是逻辑与运算符。
#![allow(unused)]
fn main() {
// Rust: && is just boolean AND
let a = true && false; // false
// Rust has NO rvalue references, no universal references, no perfect forwarding.
// Instead:
// - Move is the default for non-Copy types (no std::move needed)
// - Generics + trait bounds replace universal references
// - No temporary-binding distinction — values are values
fn process(w: Widget) { } // Takes ownership (like C++ value param + implicit move)
fn process_ref(w: &Widget) { } // Borrows immutably (like C++ const T&)
fn process_mut(w: &mut Widget) { } // Borrows mutably (like C++ T&, but exclusive)
}
| C++ 概念 | Rust 等价 | 说明 |
|---|---|---|
T&(左值引用) | &T 或 &mut T | Rust 分为共享与独占 |
T&&(右值引用) | 直接用 T | 按值接收 = 取得所有权 |
模板中的 T&&(万能引用) | impl Trait 或 <T: Trait> | 泛型替代转发 |
std::move(x) | x(直接使用) | 移动是默认行为 |
std::forward<T>(x) | 无需等价物 | 没有万能引用可转发 |
2. 移动是位拷贝——无移动构造函数
在 C++ 中,移动是用户定义操作(移动构造/移动赋值)。在 Rust 中,移动始终是值的按位 memcpy,源被作废:
#![allow(unused)]
fn main() {
// Rust move = memcpy the bytes, mark source as invalid
let s1 = String::from("hello");
let s2 = s1; // Bytes of s1 are copied to s2's stack slot
// s1 is now invalid — compiler enforces this
// println!("{s1}"); // ❌ Compile error: value used after move
}
// C++ move = call the move constructor (user-defined!)
std::string s1 = "hello";
std::string s2 = std::move(s1); // Calls string's move ctor
// s1 is now a "valid but unspecified state" zombie
std::cout << s1; // Compiles! Prints... something (empty string, usually)
后果:
- Rust 无需 Rule of Five(无需定义拷贝构造、移动构造、拷贝赋值、移动赋值、析构)
- 没有移动后「僵尸」状态——编译器直接禁止访问
- 移动无需考虑
noexcept——按位拷贝不会抛异常
3. 自动解引用:编译器穿透间接层
Rust 通过 Deref Trait 自动解引用多层指针/包装。C++ 无直接等价物:
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
// Nested wrapping: Arc<Mutex<Vec<String>>>
let data = Arc::new(Mutex::new(vec!["hello".to_string()]));
// In C++, you'd need explicit unlocking and manual dereferencing at each layer.
// In Rust, the compiler auto-derefs through Arc → Mutex → MutexGuard → Vec:
let guard = data.lock().unwrap(); // Arc auto-derefs to Mutex
let first: &str = &guard[0]; // MutexGuard→Vec (Deref), Vec[0] (Index),
// &String→&str (Deref coercion)
println!("First: {first}");
// Method calls also auto-deref:
let boxed_string = Box::new(String::from("hello"));
println!("Length: {}", boxed_string.len()); // Box→String, then String::len()
// No need for (*boxed_string).len() or boxed_string->len()
}
Deref 强制转换也适用于函数参数——编译器插入解引用使类型匹配:
fn greet(name: &str) {
println!("Hello, {name}");
}
fn main() {
let owned = String::from("Alice");
let boxed = Box::new(String::from("Bob"));
let arced = std::sync::Arc::new(String::from("Carol"));
greet(&owned); // &String → &str (1 deref coercion)
greet(&boxed); // &Box<String> → &String → &str (2 deref coercions)
greet(&arced); // &Arc<String> → &String → &str (2 deref coercions)
greet("Dave"); // &str already — no coercion needed
}
// In C++ you'd need .c_str() or explicit conversions for each case.
Deref 链:调用 x.method() 时,Rust 的方法解析先尝试接收者类型 T,再 &T、&mut T。若无匹配,则通过 Deref Trait 解引用并对目标类型重复。可穿透多层——因此 Box<Vec<T>> 能像 Vec<T> 一样「直接可用」。Deref 强制转换(用于函数参数)是相关但独立的机制,将 &Box<String> 自动转为 &str,通过链式 Deref 实现。
4. 无空引用,无可选引用
// C++: references can't be null, but pointers can, and the distinction is blurry
Widget& ref = *ptr; // If ptr is null → UB
Widget* opt = nullptr; // "optional" reference via pointer
#![allow(unused)]
fn main() {
// Rust: references are ALWAYS valid — guaranteed by the borrow checker
// No way to create a null or dangling reference in safe code
let r: &i32 = &42; // Always valid
// "Optional reference" is explicit:
let opt: Option<&Widget> = None; // Clear intent, no null pointer
if let Some(w) = opt {
w.do_something(); // Only reachable when present
}
}
5. 引用不能重新绑定目标
// C++: a reference is an alias — it can't be rebound
int a = 1, b = 2;
int& r = a;
r = b; // This ASSIGNS b's value to a — it does NOT rebind r!
// a is now 2, r still refers to a
#![allow(unused)]
fn main() {
// Rust: let bindings can shadow, but references follow different rules
let a = 1;
let b = 2;
let r = &a;
// r = &b; // ❌ Cannot assign to immutable variable
let r = &b; // ✅ But you can SHADOW r with a new binding
// The old binding is gone, not reseated
// With mut:
let mut r = &a;
r = &b; // ✅ r now points to b — this IS rebinding (not assignment through)
}
心智模型:在 C++ 中,引用是某一对象的永久别名。 在 Rust 中,引用是带生命周期保证的指针值, 遵循普通变量绑定规则——默认可变需
mut才能重新绑定。
Rust 枚举类型
你将学到: Rust 枚举作为可辨识联合体(tagged union 的正确做法)、用于穷尽模式匹配的
match,以及枚举如何以编译器强制安全的方式替代 C++ 类层次与 C 的 tagged union。
- 枚举类型是可辨识联合体,即若干可能类型的和类型,带标识具体变体的标签
- 面向 C 开发者:Rust 枚举可携带数据(tagged union 的正确做法——编译器跟踪当前活跃变体)
- 面向 C++ 开发者:Rust 枚举类似
std::variant,但有穷尽模式匹配、无std::get异常、无std::visit样板代码 enum的大小等于最大可能类型的大小。各变体彼此无关,可有完全不同的类型enum是语言最强大特性之一——在 C++ 中可替代整棵类层次(案例研究中有更多说明)
fn main() {
enum Numbers {
Zero,
SmallNumber(u8),
BiggerNumber(u32),
EvenBiggerNumber(u64),
}
let a = Numbers::Zero;
let b = Numbers::SmallNumber(42);
let c : Numbers = a; // Ok -- the type of a is Numbers
let d : Numbers = b; // Ok -- the type of b is Numbers
}
Rust match 语句
- Rust 的
match相当于加强版 C 的switchmatch可用于简单数据类型、struct、enum的模式匹配match必须穷尽,即覆盖给定type的所有可能情况。_可作「其余所有」的通配符match可产生值,但各分支(=>)必须返回相同类型的值
fn main() {
let x = 42;
// In this case, the _ covers all numbers except the ones explicitly listed
let is_secret_of_life = match x {
42 => true, // return type is boolean value
_ => false, // return type boolean value
// This won't compile because return type isn't boolean
// _ => 0
};
println!("{is_secret_of_life}");
}
Rust match 语句
match支持范围、布尔过滤与if守卫
fn main() {
let x = 42;
match x {
// Note that the =41 ensures the inclusive range
0..=41 => println!("Less than the secret of life"),
42 => println!("Secret of life"),
_ => println!("More than the secret of life"),
}
let y = 100;
match y {
100 if x == 43 => println!("y is 100% not secret of life"),
100 if x == 42 => println!("y is 100% secret of life"),
_ => (), // Do nothing
}
}
Rust match 语句
match常与enum结合- match 可将内含值绑定到变量。若值不关心,用
_ matches!宏可匹配特定变体
- match 可将内含值绑定到变量。若值不关心,用
fn main() {
enum Numbers {
Zero,
SmallNumber(u8),
BiggerNumber(u32),
EvenBiggerNumber(u64),
}
let b = Numbers::SmallNumber(42);
match b {
Numbers::Zero => println!("Zero"),
Numbers::SmallNumber(value) => println!("Small number {value}"),
Numbers::BiggerNumber(_) | Numbers::EvenBiggerNumber(_) => println!("Some BiggerNumber or EvenBiggerNumber"),
}
// Boolean test for specific variants
if matches!(b, Numbers::Zero | Numbers::SmallNumber(_)) {
println!("Matched Zero or small number");
}
}
Rust match 语句
match也可通过解构与切片匹配
fn main() {
struct Foo {
x: (u32, bool),
y: u32
}
let f = Foo {x: (42, true), y: 100};
match f {
// Capture the value of x into a variable called tuple
Foo{y: 100, x : tuple} => println!("Matched x: {tuple:?}"),
_ => ()
}
let a = [40, 41, 42];
match a {
// Last element of slice must be 42. @ is used to bind the match
[rest @ .., 42] => println!("{rest:?}"),
// First element of the slice must be 42. @ is used to bind the match
[42, rest @ ..] => println!("{rest:?}"),
_ => (),
}
}
练习:用 match 与 enum 实现加减
🟢 入门
- 编写函数,对无符号 64 位整数实现算术运算
- 步骤 1:定义运算枚举:
#![allow(unused)]
fn main() {
enum Operation {
Add(u64, u64),
Subtract(u64, u64),
}
}
- 步骤 2:定义结果枚举:
#![allow(unused)]
fn main() {
enum CalcResult {
Ok(u64), // Successful result
Invalid(String), // Error message for invalid operations
}
}
- 步骤 3:实现
calculate(op: Operation) -> CalcResult- Add:返回 Ok(和)
- Subtract:若第一个 >= 第二个则返回 Ok(差),否则 Invalid(“Underflow”)
- 提示:在函数中使用模式匹配:
#![allow(unused)]
fn main() {
match op {
Operation::Add(a, b) => { /* your code */ },
Operation::Subtract(a, b) => { /* your code */ },
}
}
Solution (click to expand)
enum Operation {
Add(u64, u64),
Subtract(u64, u64),
}
enum CalcResult {
Ok(u64),
Invalid(String),
}
fn calculate(op: Operation) -> CalcResult {
match op {
Operation::Add(a, b) => CalcResult::Ok(a + b),
Operation::Subtract(a, b) => {
if a >= b {
CalcResult::Ok(a - b)
} else {
CalcResult::Invalid("Underflow".to_string())
}
}
}
}
fn main() {
match calculate(Operation::Add(10, 20)) {
CalcResult::Ok(result) => println!("10 + 20 = {result}"),
CalcResult::Invalid(msg) => println!("Error: {msg}"),
}
match calculate(Operation::Subtract(5, 10)) {
CalcResult::Ok(result) => println!("5 - 10 = {result}"),
CalcResult::Invalid(msg) => println!("Error: {msg}"),
}
}
// Output:
// 10 + 20 = 30
// Error: Underflow
Rust 关联方法
impl可为struct、enum等类型定义关联方法- 方法可选地接受
self参数。self概念上类似 C 中把结构体指针作为首参,或 C++ 中的this - 对
self的引用可为不可变(默认&self)、可变(&mut self)或self(转移所有权) Self关键字可作类型简写
- 方法可选地接受
struct Point {x: u32, y: u32}
impl Point {
fn new(x: u32, y: u32) -> Self {
Point {x, y}
}
fn increment_x(&mut self) {
self.x += 1;
}
}
fn main() {
let mut p = Point::new(10, 20);
p.increment_x();
}
练习:Point 的 add 与 transform
🟡 中级 — 需理解方法签名中的移动 vs 借用
- 为
Point实现下列关联方法add()接收另一个Point,原地增加 x、y(提示:使用&mut self)transform()消费现有Point(提示:使用self),返回 x、y 平方后的新Point
Solution (click to expand)
struct Point { x: u32, y: u32 }
impl Point {
fn new(x: u32, y: u32) -> Self {
Point { x, y }
}
fn add(&mut self, other: &Point) {
self.x += other.x;
self.y += other.y;
}
fn transform(self) -> Point {
Point { x: self.x * self.x, y: self.y * self.y }
}
}
fn main() {
let mut p1 = Point::new(2, 3);
let p2 = Point::new(10, 20);
p1.add(&p2);
println!("After add: x={}, y={}", p1.x, p1.y); // x=12, y=23
let p3 = p1.transform();
println!("After transform: x={}, y={}", p3.x, p3.y); // x=144, y=529
// p1 is no longer accessible — transform() consumed it
}
Rust 内存管理
你将学到: Rust 的所有权系统——语言中最重要的概念。读完本章你将理解移动语义、借用规则与
DropTrait。掌握本章,Rust 其余部分会自然跟上。若感到吃力,请重读——多数 C/C++ 开发者在第二遍阅读时才会豁然开朗。
- C/C++ 的内存管理是 bug 来源:
- C:用
malloc()分配、free()释放。无悬垂指针、释放后使用、双重释放等检查 - C++:RAII(Resource Acquisition Is Initialization,资源获取即初始化)与智能指针有帮助,但
std::move(ptr)在移动后仍可编译——移动后使用是 UB
- C:用
- Rust 让 RAII 防呆:
- 移动是破坏性的——编译器拒绝让你再碰已移动变量
- 无需 Rule of Five(无拷贝构造、移动构造、拷贝赋值、移动赋值、析构)
- Rust 完全控制内存分配,但在编译期强制安全
- 通过所有权、借用、可变性与生命周期等机制组合实现
- Rust 运行时分配可在栈与堆上进行
面向 C++ 开发者——智能指针对照:
C++ Rust 安全改进 std::unique_ptr<T>Box<T>不可能移动后使用 std::shared_ptr<T>Rc<T>(单线程)默认无引用循环 std::shared_ptr<T>(线程安全)Arc<T>显式线程安全 std::weak_ptr<T>Weak<T>必须检查有效性 裸指针 *const T/*mut T仅在 unsafe代码块中面向 C 开发者:
Box<T>替代malloc/free配对。Rc<T>替代手动引用计数。裸指针存在,但限于unsafe代码块。
Rust 所有权、借用与生命周期
- 回顾:Rust 只允许一个可变引用与多个只读引用
- 变量初始声明建立
ownership(所有权) - 后续引用从原所有者
borrow(借用)。规则是借用作用域不得超过拥有者作用域。即借用的lifetime(生命周期)不得超过拥有者的生命周期
- 变量初始声明建立
fn main() {
let a = 42; // Owner
let b = &a; // First borrow
{
let aa = 42;
let c = &a; // Second borrow; a is still in scope
// Ok: c goes out of scope here
// aa goes out of scope here
}
// let d = &aa; // Will not compile unless aa is moved to outside scope
// b implicitly goes out of scope before a
// a goes out of scope last
}
- Rust 可用多种机制向方法传参
- 按值(拷贝):通常是可 trivial 拷贝的类型(如 u8、u32、i8、i32)
- 按引用:相当于传指向实际值的指针,亦称借用;引用可为不可变(
&)或可变(&mut) - 按移动:将值的「所有权」转移给函数,调用方不能再引用原值
fn foo(x: &u32) {
println!("{x}");
}
fn bar(x: u32) {
println!("{x}");
}
fn main() {
let a = 42;
foo(&a); // By reference
bar(a); // By value (copy)
}
- Rust 禁止方法返回悬垂引用
- 方法返回的引用必须仍在作用域内
- 引用离开作用域时 Rust 会自动
drop
fn no_dangling() -> &u32 {
// lifetime of a begins here
let a = 42;
// Won't compile. lifetime of a ends here
&a
}
fn ok_reference(a: &u32) -> &u32 {
// Ok because the lifetime of a always exceeds ok_reference()
a
}
fn main() {
let a = 42; // lifetime of a begins here
let b = ok_reference(&a);
// lifetime of b ends here
// lifetime of a ends here
}
Rust 移动语义
- 默认情况下,Rust 赋值转移所有权
fn main() {
let s = String::from("Rust"); // Allocate a string from the heap
let s1 = s; // Transfer ownership to s1. s is invalid at this point
println!("{s1}");
// This will not compile
//println!("{s}");
// s1 goes out of scope here and the memory is deallocated
// s goes out of scope here, but nothing happens because it doesn't own anything
}
graph LR
subgraph "Before: let s1 = s"
S["s (stack)<br/>ptr"] -->|"owns"| H1["Heap: R u s t"]
end
subgraph "After: let s1 = s"
S_MOVED["s (stack)<br/>⚠️ MOVED"] -.->|"invalid"| H2["Heap: R u s t"]
S1["s1 (stack)<br/>ptr"] -->|"now owns"| H2
end
style S_MOVED fill:#ff6b6b,color:#000,stroke:#333
style S1 fill:#51cf66,color:#000,stroke:#333
style H2 fill:#91e5a3,color:#000,stroke:#333
let s1 = s 后,所有权转移到 s1。堆数据不动——仅栈上指针移动。s 现已无效。
Rust 移动语义与借用
fn foo(s : String) {
println!("{s}");
// The heap memory pointed to by s will be deallocated here
}
fn bar(s : &String) {
println!("{s}");
// Nothing happens -- s is borrowed
}
fn main() {
let s = String::from("Rust string move example"); // Allocate a string from the heap
foo(s); // Transfers ownership; s is invalid now
// println!("{s}"); // will not compile
let t = String::from("Rust string borrow example");
bar(&t); // t continues to hold ownership
println!("{t}");
}
Rust 移动语义与所有权
- 可通过移动转移所有权
- 移动完成后,再引用仍存在的引用是非法的
- 若不希望移动,考虑借用
struct Point {
x: u32,
y: u32,
}
fn consume_point(p: Point) {
println!("{} {}", p.x, p.y);
}
fn borrow_point(p: &Point) {
println!("{} {}", p.x, p.y);
}
fn main() {
let p = Point {x: 10, y: 20};
// Try flipping the two lines
borrow_point(&p);
consume_point(p);
}
Rust Clone
clone()方法可复制原内存,原引用仍有效(代价是双倍分配)
fn main() {
let s = String::from("Rust"); // Allocate a string from the heap
let s1 = s.clone(); // Copy the string; creates a new allocation on the heap
println!("{s1}");
println!("{s}");
// s1 goes out of scope here and the memory is deallocated
// s goes out of scope here, and the memory is deallocated
}
graph LR
subgraph "After: let s1 = s.clone()"
S["s (stack)<br/>ptr"] -->|"owns"| H1["Heap: R u s t"]
S1["s1 (stack)<br/>ptr"] -->|"owns (copy)"| H2["Heap: R u s t"]
end
style S fill:#51cf66,color:#000,stroke:#333
style S1 fill:#51cf66,color:#000,stroke:#333
style H1 fill:#91e5a3,color:#000,stroke:#333
style H2 fill:#91e5a3,color:#000,stroke:#333
clone() 创建独立堆分配。s 与 s1 均有效——各自拥有自己的副本。
Rust Copy Trait
- Rust 通过
CopyTrait 为内置类型实现拷贝语义- 例如 u8、u32、i8、i32 等。拷贝语义使用「按值传递」
- 用户定义类型可选用
derive宏自动实现CopyTrait - 新赋值后编译器会为拷贝分配空间
// Try commenting this out to see the change in let p1 = p; below
#[derive(Copy, Clone, Debug)] // We'll discuss this more later
struct Point{x: u32, y:u32}
fn main() {
let p = Point {x: 42, y: 40};
let p1 = p; // This will perform a copy now instead of move
println!("p: {p:?}");
println!("p1: {p:?}");
let p2 = p1.clone(); // Semantically the same as copy
}
Rust Drop Trait
- Rust 在作用域结束时自动调用
drop()方法drop属于名为Drop的泛型 Trait。编译器为所有类型提供默认空实现,类型可覆盖。例如String覆盖它以释放堆内存- 面向 C 开发者:替代手动
free()——资源在离开作用域时自动释放(RAII)
- 关键安全: 不能直接调用
.drop()(编译器禁止)。应使用drop(obj),将值移入函数、运行析构并阻止进一步使用——消除双重释放 bug
面向 C++ 开发者:
Drop直接对应 C++ 析构函数(~ClassName()):
C++ 析构 Rust Drop语法 ~MyClass() { ... }impl Drop for MyType { fn drop(&mut self) { ... } }调用时机 作用域结束(RAII) 作用域结束(相同) 移动时 源处于「有效但未指定」状态——仍对移动源对象调用析构 源已消失——不对移动源值调用析构 手动调用 obj.~MyClass()(危险,少用)drop(obj)(安全——取得所有权、调用drop、阻止再用)顺序 与声明相反 与声明相反(相同) Rule of Five 须管理拷贝构造、移动构造、拷贝赋值、移动赋值、析构 只需 Drop——编译器处理移动语义,Clone可选需要虚析构? 通过基指针删除时需要 否——无继承,无切片问题
struct Point {x: u32, y:u32}
// Equivalent to: ~Point() { printf("Goodbye point x:%u, y:%u\n", x, y); }
impl Drop for Point {
fn drop(&mut self) {
println!("Goodbye point x:{}, y:{}", self.x, self.y);
}
}
fn main() {
let p = Point{x: 42, y: 42};
{
let p1 = Point{x:43, y: 43};
println!("Exiting inner block");
// p1.drop() called here — like C++ end-of-scope destructor
}
println!("Exiting main");
// p.drop() called here
}
练习:Move、Copy 与 Drop
🟡 中级 — 自由实验;编译器会引导你
- 用
Point自行实验,在#[derive(Debug)]中有无Copy的区别,确保理解移动 vs 拷贝。若有疑问请提问 - 为
Point实现自定义Drop,在drop中将 x、y 置 0。此模式可用于释放锁等资源
struct Point{x: u32, y: u32}
fn main() {
// Create Point, assign it to a different variable, create a new scope,
// pass point to a function, etc.
}
Solution (click to expand)
#[derive(Debug)]
struct Point { x: u32, y: u32 }
impl Drop for Point {
fn drop(&mut self) {
println!("Dropping Point({}, {})", self.x, self.y);
self.x = 0;
self.y = 0;
// Note: setting to 0 in drop demonstrates the pattern,
// but you can't observe these values after drop completes
}
}
fn consume(p: Point) {
println!("Consuming: {:?}", p);
// p is dropped here
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = p1; // Move — p1 is no longer valid
// println!("{:?}", p1); // Won't compile: p1 was moved
{
let p3 = Point { x: 30, y: 40 };
println!("p3 in inner scope: {:?}", p3);
// p3 is dropped here (end of scope)
}
consume(p2); // p2 is moved into consume and dropped there
// println!("{:?}", p2); // Won't compile: p2 was moved
// Now try: add #[derive(Copy, Clone)] to Point (and remove the Drop impl)
// and observe how p1 remains valid after let p2 = p1;
}
// Output:
// p3 in inner scope: Point { x: 30, y: 40 }
// Dropping Point(30, 40)
// Consuming: Point { x: 10, y: 20 }
// Dropping Point(10, 20)
Rust 生命周期与借用
你将学到: Rust 生命周期系统如何确保引用永不悬垂——从隐式生命周期、显式标注到三条省略规则(使多数代码无需标注)。理解生命周期后再进入下一节的智能指针。
- Rust 强制单一可变引用与任意数量不可变引用
- 任何引用的生命周期至少与原拥有者生命周期一样长。这些是隐式生命周期,由编译器推断(见 https://doc.rust-lang.org/nomicon/lifetime-elision.html)
fn borrow_mut(x: &mut u32) {
*x = 43;
}
fn main() {
let mut x = 42;
let y = &mut x;
borrow_mut(y);
let _z = &x; // Permitted because the compiler knows y isn't subsequently used
//println!("{y}"); // Will not compile if this is uncommented
borrow_mut(&mut x); // Permitted because _z isn't used
let z = &x; // Ok -- mutable borrow of x ended after borrow_mut() returned
println!("{z}");
}
Rust 生命周期标注
- 处理多个生命周期时需要显式生命周期标注
- 生命周期用
'表示,可为任意标识符('a、'b、'static等) - 编译器无法推断引用应存活多久时需要帮助
- 生命周期用
- 常见场景:函数返回引用,但来自哪个输入?
#[derive(Debug)]
struct Point {x: u32, y: u32}
// Without lifetime annotation, this won't compile:
// fn left_or_right(pick_left: bool, left: &Point, right: &Point) -> &Point
// With lifetime annotation - all references share the same lifetime 'a
fn left_or_right<'a>(pick_left: bool, left: &'a Point, right: &'a Point) -> &'a Point {
if pick_left { left } else { right }
}
// More complex: different lifetimes for inputs
fn get_x_coordinate<'a, 'b>(p1: &'a Point, _p2: &'b Point) -> &'a u32 {
&p1.x // Return value lifetime tied to p1, not p2
}
fn main() {
let p1 = Point {x: 20, y: 30};
let result;
{
let p2 = Point {x: 42, y: 50};
result = left_or_right(true, &p1, &p2);
// This works because we use result before p2 goes out of scope
println!("Selected: {result:?}");
}
// This would NOT work - result references p2 which is now gone:
// println!("After scope: {result:?}");
}
Rust 生命周期标注
- 数据结构中的引用也需要生命周期标注
use std::collections::HashMap;
#[derive(Debug)]
struct Point {x: u32, y: u32}
struct Lookup<'a> {
map: HashMap<u32, &'a Point>,
}
fn main() {
let p = Point{x: 42, y: 42};
let p1 = Point{x: 50, y: 60};
let mut m = Lookup {map : HashMap::new()};
m.map.insert(0, &p);
m.map.insert(1, &p1);
{
let p3 = Point{x: 60, y:70};
//m.map.insert(3, &p3); // Will not compile
// p3 is dropped here, but m will outlive
}
for (k, v) in m.map {
println!("{v:?}");
}
// m is dropped here
// p1 and p are dropped here in that order
}
练习:带生命周期的 first word
🟢 入门 — 实践生命周期省略
编写函数 fn first_word(s: &str) -> &str,返回字符串中第一个空白分隔的单词。思考为何无需显式生命周期标注即可编译(提示:省略规则 #1 与 #2)。
Solution (click to expand)
fn first_word(s: &str) -> &str {
// The compiler applies elision rules:
// Rule 1: input &str gets lifetime 'a → fn first_word(s: &'a str) -> &str
// Rule 2: single input lifetime → output gets same → fn first_word(s: &'a str) -> &'a str
match s.find(' ') {
Some(pos) => &s[..pos],
None => s,
}
}
fn main() {
let text = "hello world foo";
let word = first_word(text);
println!("First word: {word}"); // "hello"
let single = "onlyone";
println!("First word: {}", first_word(single)); // "onlyone"
}
练习:带生命周期的切片存储
🟡 中级 — 首次接触生命周期标注
- 创建存储
&str切片引用的结构体- 创建长
&str,在结构体中存储其切片引用 - 编写接受该结构体并返回所含切片的函数
- 创建长
// TODO: Create a structure to store a reference to a slice
struct SliceStore {
}
fn main() {
let s = "This is long string";
let s1 = &s[0..];
let s2 = &s[1..2];
// let slice = struct SliceStore {...};
// let slice2 = struct SliceStore {...};
}
Solution (click to expand)
struct SliceStore<'a> {
slice: &'a str,
}
impl<'a> SliceStore<'a> {
fn new(slice: &'a str) -> Self {
SliceStore { slice }
}
fn get_slice(&self) -> &'a str {
self.slice
}
}
fn main() {
let s = "This is a long string";
let store1 = SliceStore::new(&s[0..4]); // "This"
let store2 = SliceStore::new(&s[5..7]); // "is"
println!("store1: {}", store1.get_slice());
println!("store2: {}", store2.get_slice());
}
// Output:
// store1: This
// store2: is
生命周期省略规则深入
C 程序员常问:「生命周期这么重要,为何多数 Rust 函数没有 'a 标注?」答案是生命周期省略——编译器用三条确定性规则自动推断生命周期。
三条省略规则
Rust 编译器按顺序将这些规则应用于函数签名。应用后若所有输出生命周期均已确定,则无需标注。
flowchart TD
A["Function signature<br/>with references"] --> R1
R1["Rule 1: Each input<br/>reference gets its own<br/>lifetime<br/><br/>fn f(&str, &str)<br/>→ fn f<'a,'b>(&'a str,<br/>&'b str)"]
R1 --> R2
R2["Rule 2: If exactly ONE<br/>input lifetime, assign it<br/>to ALL outputs<br/><br/>fn f(&str) → &str<br/>→ fn f<'a>(&'a str)<br/>→ &'a str"]
R2 --> R3
R3["Rule 3: If one input is<br/>&self or &mut self,<br/>assign its lifetime to<br/>ALL outputs<br/><br/>fn f(&self, &str) → &str<br/>→ fn f<'a>(&'a self, &str)<br/>→ &'a str"]
R3 --> CHECK{{"All output<br/>lifetimes<br/>determined?"}}
CHECK -->|Yes| OK["✅ No annotations<br/>needed"]
CHECK -->|No| ERR["❌ Compile error:<br/>must annotate<br/>manually"]
style OK fill:#91e5a3,color:#000
style ERR fill:#ff6b6b,color:#000
逐条规则示例
规则 1 — 每个输入引用获得独立生命周期参数:
#![allow(unused)]
fn main() {
// What you write:
fn first_word(s: &str) -> &str { ... }
// What the compiler sees after Rule 1:
fn first_word<'a>(s: &'a str) -> &str { ... }
// Only one input lifetime → Rule 2 applies
}
规则 2 — 单一输入生命周期传播到所有输出:
#![allow(unused)]
fn main() {
// After Rule 2:
fn first_word<'a>(s: &'a str) -> &'a str { ... }
// ✅ All output lifetimes determined — no annotation needed!
}
规则 3 — &self 的生命周期传播到输出:
#![allow(unused)]
fn main() {
// What you write:
impl SliceStore<'_> {
fn get_slice(&self) -> &str { self.slice }
}
// What the compiler sees after Rules 1 + 3:
impl SliceStore<'_> {
fn get_slice<'a>(&'a self) -> &'a str { self.slice }
}
// ✅ No annotation needed — &self lifetime used for output
}
省略失败时 — 必须标注:
#![allow(unused)]
fn main() {
// Two input references, no &self → Rules 2 and 3 don't apply
// fn longest(a: &str, b: &str) -> &str ← WON'T COMPILE
// Fix: tell the compiler which input the output borrows from
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
}
C 程序员心智模型
在 C 中,每个指针独立——程序员在脑中跟踪每个指针指向哪块分配,编译器完全信任你。在 Rust 中,生命周期使这种跟踪显式且由编译器验证:
| C | Rust | 发生什么 |
|---|---|---|
char* get_name(struct User* u) | fn get_name(&self) -> &str | 规则 3 省略:输出从 self 借用 |
char* concat(char* a, char* b) | fn concat<'a>(a: &'a str, b: &'a str) -> &'a str | 必须标注——两个输入 |
void process(char* in, char* out) | fn process(input: &str, output: &mut String) | 无输出引用——无需生命周期 |
char* buf; /* who owns this? */ | 生命周期错误则编译失败 | 编译器捕获悬垂指针 |
'static 生命周期
'static 表示引用在整个程序运行期间有效。相当于 C 的全局变量或字符串字面量:
#![allow(unused)]
fn main() {
// String literals are always 'static — they live in the binary's read-only section
let s: &'static str = "hello"; // Same as: static const char* s = "hello"; in C
// Constants are also 'static
static GREETING: &str = "hello";
// Common in trait bounds for thread spawning:
fn spawn<F: FnOnce() + Send + 'static>(f: F) { /* ... */ }
// 'static here means: "the closure must not borrow any local variables"
// (either move them in, or use only 'static data)
}
练习:预测省略结果
🟡 中级
对下列每个函数签名,预测编译器能否省略生命周期。 若不能,添加必要标注:
#![allow(unused)]
fn main() {
// 1. Can the compiler elide?
fn trim_prefix(s: &str) -> &str { &s[1..] }
// 2. Can the compiler elide?
fn pick(flag: bool, a: &str, b: &str) -> &str {
if flag { a } else { b }
}
// 3. Can the compiler elide?
struct Parser { data: String }
impl Parser {
fn next_token(&self) -> &str { &self.data[..5] }
}
// 4. Can the compiler elide?
fn split_at(s: &str, pos: usize) -> (&str, &str) {
(&s[..pos], &s[pos..])
}
}
Solution (click to expand)
// 1. YES — Rule 1 gives 'a to s, Rule 2 propagates to output
fn trim_prefix(s: &str) -> &str { &s[1..] }
// 2. NO — Two input references, no &self. Must annotate:
fn pick<'a>(flag: bool, a: &'a str, b: &'a str) -> &'a str {
if flag { a } else { b }
}
// 3. YES — Rule 1 gives 'a to &self, Rule 3 propagates to output
impl Parser {
fn next_token(&self) -> &str { &self.data[..5] }
}
// 4. YES — Rule 1 gives 'a to s (only one input reference),
// Rule 2 propagates to BOTH outputs. Both slices borrow from s.
fn split_at(s: &str, pos: usize) -> (&str, &str) {
(&s[..pos], &s[pos..])
}
Rust Box<T>
你将学到: Rust 智能指针类型——堆分配的
Box<T>、共享所有权的Rc<T>,以及内部可变性的Cell<T>/RefCell<T>。这些建立在前几节所有权与生命周期之上。还会简要介绍用Weak<T>打破引用循环。
为何用 Box<T>? C 中用 malloc/free 做堆分配。C++ 中 std::unique_ptr<T> 包装 new/delete。Rust 的 Box<T> 等价——堆分配、单一所有者指针,离开作用域自动释放。与 malloc 不同,没有配对的 free 可忘。与 unique_ptr 不同,不可能移动后使用——编译器完全阻止。
何时用 Box vs 栈分配:
-
内含类型很大,不想在栈上拷贝
-
需要递归类型(如包含自身的链表节点)
-
需要 Trait 对象(
Box<dyn Trait>) -
Box<T>可创建指向堆分配类型的指针。指针大小固定,与<T>类型无关
fn main() {
// Creates a pointer to an integer (with value 42) created on the heap
let f = Box::new(42);
println!("{} {}", *f, f);
// Cloning a box creates a new heap allocation
let mut g = f.clone();
*g = 43;
println!("{f} {g}");
// g and f go out of scope here and are automatically deallocated
}
graph LR
subgraph "Stack"
F["f: Box<i32>"]
G["g: Box<i32>"]
end
subgraph "Heap"
HF["42"]
HG["43"]
end
F -->|"owns"| HF
G -->|"owns (cloned)"| HG
style F fill:#51cf66,color:#000,stroke:#333
style G fill:#51cf66,color:#000,stroke:#333
style HF fill:#91e5a3,color:#000,stroke:#333
style HG fill:#91e5a3,color:#000,stroke:#333
所有权与借用可视化
C/C++ vs Rust:指针与所有权管理
// C - Manual memory management, potential issues
void c_pointer_problems() {
int* ptr1 = malloc(sizeof(int));
*ptr1 = 42;
int* ptr2 = ptr1; // Both point to same memory
int* ptr3 = ptr1; // Three pointers to same memory
free(ptr1); // Frees the memory
*ptr2 = 43; // Use after free - undefined behavior!
*ptr3 = 44; // Use after free - undefined behavior!
}
面向 C++ 开发者: 智能指针有帮助,但不能防止所有问题:
// C++ - Smart pointers help, but don't prevent all issues void cpp_pointer_issues() { auto ptr1 = std::make_unique<int>(42); // auto ptr2 = ptr1; // Compile error: unique_ptr not copyable auto ptr2 = std::move(ptr1); // OK: ownership transferred // But C++ still allows use-after-move: // std::cout << *ptr1; // Compiles! But undefined behavior! // shared_ptr aliasing: auto shared1 = std::make_shared<int>(42); auto shared2 = shared1; // Both own the data // Who "really" owns it? Neither. Ref count overhead everywhere. }
#![allow(unused)]
fn main() {
// Rust - Ownership system prevents these issues
fn rust_ownership_safety() {
let data = Box::new(42); // data owns the heap allocation
let moved_data = data; // Ownership transferred to moved_data
// data is no longer accessible - compile error if used
let borrowed = &moved_data; // Immutable borrow
println!("{}", borrowed); // Safe to use
// moved_data automatically freed when it goes out of scope
}
}
graph TD
subgraph "C/C++ Memory Management Issues"
CP1["int* ptr1"] --> CM["Heap Memory<br/>value: 42"]
CP2["int* ptr2"] --> CM
CP3["int* ptr3"] --> CM
CF["free(ptr1)"] --> CM_F["[ERROR] Freed Memory"]
CP2 -.->|"Use after free<br/>Undefined Behavior"| CM_F
CP3 -.->|"Use after free<br/>Undefined Behavior"| CM_F
end
subgraph "Rust Ownership System"
RO1["data: Box<i32>"] --> RM["Heap Memory<br/>value: 42"]
RO1 -.->|"Move ownership"| RO2["moved_data: Box<i32>"]
RO2 --> RM
RO1_X["data: [WARNING] MOVED<br/>Cannot access"]
RB["&moved_data<br/>Immutable borrow"] -.->|"Safe reference"| RM
RD["Drop automatically<br/>when out of scope"] --> RM
end
style CM_F fill:#ff6b6b,color:#000
style CP2 fill:#ff6b6b,color:#000
style CP3 fill:#ff6b6b,color:#000
style RO1_X fill:#ffa07a,color:#000
style RO2 fill:#51cf66,color:#000
style RB fill:#91e5a3,color:#000
style RD fill:#91e5a3,color:#000
借用规则可视化
#![allow(unused)]
fn main() {
fn borrowing_rules_example() {
let mut data = vec![1, 2, 3, 4, 5];
// Multiple immutable borrows - OK
let ref1 = &data;
let ref2 = &data;
println!("{:?} {:?}", ref1, ref2); // Both can be used
// Mutable borrow - exclusive access
let ref_mut = &mut data;
ref_mut.push(6);
// ref1 and ref2 can't be used while ref_mut is active
// After ref_mut is done, immutable borrows work again
let ref3 = &data;
println!("{:?}", ref3);
}
}
graph TD
subgraph "Rust Borrowing Rules"
D["mut data: Vec<i32>"]
subgraph "Phase 1: Multiple Immutable Borrows [OK]"
IR1["&data (ref1)"]
IR2["&data (ref2)"]
D --> IR1
D --> IR2
IR1 -.->|"Read-only access"| MEM1["Memory: [1,2,3,4,5]"]
IR2 -.->|"Read-only access"| MEM1
end
subgraph "Phase 2: Exclusive Mutable Borrow [OK]"
MR["&mut data (ref_mut)"]
D --> MR
MR -.->|"Exclusive read/write"| MEM2["Memory: [1,2,3,4,5,6]"]
BLOCK["[ERROR] Other borrows blocked"]
end
subgraph "Phase 3: Immutable Borrows Again [OK]"
IR3["&data (ref3)"]
D --> IR3
IR3 -.->|"Read-only access"| MEM3["Memory: [1,2,3,4,5,6]"]
end
end
subgraph "What C/C++ Allows (Dangerous)"
CP["int* ptr"]
CP2["int* ptr2"]
CP3["int* ptr3"]
CP --> CMEM["Same Memory"]
CP2 --> CMEM
CP3 --> CMEM
RACE["[ERROR] Data races possible<br/>[ERROR] Use after free possible"]
end
style MEM1 fill:#91e5a3,color:#000
style MEM2 fill:#91e5a3,color:#000
style MEM3 fill:#91e5a3,color:#000
style BLOCK fill:#ffa07a,color:#000
style RACE fill:#ff6b6b,color:#000
style CMEM fill:#ff6b6b,color:#000
内部可变性:Cell<T> 与 RefCell<T>
回顾:Rust 中变量默认可变。有时希望类型大部分只读,但允许单个字段可写。
#![allow(unused)]
fn main() {
struct Employee {
employee_id : u64, // This must be immutable
on_vacation: bool, // What if we wanted to permit write-access to this field, but make employee_id immutable?
}
}
- 回顾:Rust 允许对变量一个可变引用与任意数量不可变引用——在编译期强制
- 若希望传递不可变的员工向量,但允许更新
on_vacation字段,同时确保employee_id不可变,怎么办?
Cell<T> — 适用于 Copy 类型的内部可变性
Cell<T>提供内部可变性,即在否则只读的引用上对某些元素可写- 通过拷贝值进出实现(
.get()要求T: Copy)
RefCell<T> — 运行时借用检查的内部可变性
RefCell<T>提供基于引用的变体- 在运行时而非编译期执行 Rust 借用检查
- 允许单个可变借用,但若仍有其他引用活跃会** panic**
- 用
.borrow()不可变访问,.borrow_mut()可变访问
何时选 Cell vs RefCell
| 标准 | Cell<T> | RefCell<T> |
|---|---|---|
| 适用类型 | Copy 类型(整数、bool、浮点) | 任意类型(String、Vec、结构体) |
| 访问模式 | 拷贝进出(.get()、.set()) | 原地借用(.borrow()、.borrow_mut()) |
| 失败模式 | 不会失败——无运行时检查 | 可变借用时若另有活跃借用会** panic** |
| 开销 | 零——仅拷贝字节 | 小——运行时跟踪借用状态 |
| 使用场景 | 不可变结构体内需要可变标志、计数器或小值 | 不可变结构体内需修改 String、Vec 或复杂类型 |
共享所有权:Rc<T>
Rc<T> 允许对不可变数据进行引用计数共享所有权。若希望同一 Employee 存于多处而不拷贝,怎么办?
#[derive(Debug)]
struct Employee {
employee_id: u64,
}
fn main() {
let mut us_employees = vec![];
let mut all_global_employees = Vec::<Employee>::new();
let employee = Employee { employee_id: 42 };
us_employees.push(employee);
// Won't compile — employee was already moved
//all_global_employees.push(employee);
}
Rc<T> 通过共享不可变访问解决该问题:
- 内含类型自动解引用
- 引用计数为 0 时类型被 drop
use std::rc::Rc;
#[derive(Debug)]
struct Employee {employee_id: u64}
fn main() {
let mut us_employees = vec![];
let mut all_global_employees = vec![];
let employee = Employee { employee_id: 42 };
let employee_rc = Rc::new(employee);
us_employees.push(employee_rc.clone());
all_global_employees.push(employee_rc.clone());
let employee_one = all_global_employees.get(0); // Shared immutable reference
for e in us_employees {
println!("{}", e.employee_id); // Shared immutable reference
}
println!("{employee_one:?}");
}
面向 C++ 开发者:智能指针对照
C++ 智能指针 Rust 等价 关键差异 std::unique_ptr<T>Box<T>Rust 版本是默认——移动是语言级,非可选 std::shared_ptr<T>Rc<T>(单线程)/Arc<T>(多线程)Rc无原子开销;跨线程共享才用Arcstd::weak_ptr<T>Weak<T>(来自Rc::downgrade()或Arc::downgrade())相同用途:打破引用循环 关键区别:C++ 中你选择用智能指针。Rust 中拥有值(
T)与借用(&T)覆盖多数场景——仅在需要堆分配或共享所有权时用Box/Rc/Arc。
用 Weak<T> 打破引用循环
Rc<T> 使用引用计数——若两个 Rc 互相指向,两者都不会被 drop(形成循环)。Weak<T> 解决此问题:
use std::rc::{Rc, Weak};
struct Node {
value: i32,
parent: Option<Weak<Node>>, // Weak reference — doesn't prevent drop
}
fn main() {
let parent = Rc::new(Node { value: 1, parent: None });
let child = Rc::new(Node {
value: 2,
parent: Some(Rc::downgrade(&parent)), // Weak ref to parent
});
// To use a Weak, try to upgrade it — returns Option<Rc<T>>
if let Some(parent_rc) = child.parent.as_ref().unwrap().upgrade() {
println!("Parent value: {}", parent_rc.value);
}
println!("Parent strong count: {}", Rc::strong_count(&parent)); // 1, not 2
}
Weak<T>在 避免过多 clone() 中有更详细讲解。目前要点:在树/图结构的「反向引用」中使用Weak,避免内存泄漏。
将 Rc 与内部可变性结合
Rc<T>(共享所有权)与 Cell<T> 或 RefCell<T>(内部可变性)结合时威力更大。多个所有者可读写共享数据:
| 模式 | 使用场景 |
|---|---|
Rc<RefCell<T>> | 共享可变数据(单线程) |
Arc<Mutex<T>> | 共享可变数据(多线程——见 ch13) |
Rc<Cell<T>> | 共享可变 Copy 类型(简单标志、计数器) |
练习:共享所有权与内部可变性
🟡 中级
- Part 1(Rc):创建含
employee_id: u64与name: String的Employee结构体。放入Rc<Employee>并 clone 到两个Vec(us_employees与global_employees)。从两个向量打印以展示共享同一数据。 - Part 2(Cell):为
Employee添加on_vacation: Cell<bool>字段。将不可变&Employee引用传给函数,在函数内切换on_vacation——无需使引用可变。 - Part 3(RefCell):将
name: String换为name: RefCell<String>,编写函数通过&Employee(不可变引用)向员工姓名追加后缀。
Starter code:
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Debug)]
struct Employee {
employee_id: u64,
name: RefCell<String>,
on_vacation: Cell<bool>,
}
fn toggle_vacation(emp: &Employee) {
// TODO: Flip on_vacation using Cell::set()
}
fn append_title(emp: &Employee, title: &str) {
// TODO: Borrow name mutably via RefCell and push_str the title
}
fn main() {
// TODO: Create an employee, wrap in Rc, clone into two Vecs,
// call toggle_vacation and append_title, print results
}
Solution (click to expand)
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Debug)]
struct Employee {
employee_id: u64,
name: RefCell<String>,
on_vacation: Cell<bool>,
}
fn toggle_vacation(emp: &Employee) {
emp.on_vacation.set(!emp.on_vacation.get());
}
fn append_title(emp: &Employee, title: &str) {
emp.name.borrow_mut().push_str(title);
}
fn main() {
let emp = Rc::new(Employee {
employee_id: 42,
name: RefCell::new("Alice".to_string()),
on_vacation: Cell::new(false),
});
let mut us_employees = vec![];
let mut global_employees = vec![];
us_employees.push(Rc::clone(&emp));
global_employees.push(Rc::clone(&emp));
// Toggle vacation through an immutable reference
toggle_vacation(&emp);
println!("On vacation: {}", emp.on_vacation.get()); // true
// Append title through an immutable reference
append_title(&emp, ", Sr. Engineer");
println!("Name: {}", emp.name.borrow()); // "Alice, Sr. Engineer"
// Both Vecs see the same data (Rc shares ownership)
println!("US: {:?}", us_employees[0].name.borrow());
println!("Global: {:?}", global_employees[0].name.borrow());
println!("Rc strong count: {}", Rc::strong_count(&emp));
}
// Output:
// On vacation: true
// Name: Alice, Sr. Engineer
// US: "Alice, Sr. Engineer"
// Global: "Alice, Sr. Engineer"
// Rc strong count: 3
Rust crate 与 module
你将学到: Rust 如何用 module 与 crate 组织代码——默认私有的可见性、
pub修饰符、workspace,以及crates.io生态。替代 C/C++ 头文件、#include与 CMake 依赖管理。
- module 是 crate 内代码的基本组织单元
- 每个源文件(.rs)是一个 module,可用
mod创建嵌套 module - (子)module 中所有类型默认私有,除非显式标记为
pub(公开),否则同一 crate 内也不对外可见。pub范围可进一步限制为pub(crate)等 - 即使类型公开,也须用
use导入才会在另一 module 作用域可见。子 module 可用use super::引用父作用域类型 - 源文件(.rs)不会自动纳入 crate,除非在
main.rs(可执行)或lib.rs中显式列出
- 每个源文件(.rs)是一个 module,可用
练习:module 与函数
- 修改 hello world 以调用另一函数
- 如前所述,函数用
fn定义。->声明函数返回值(默认为 void),类型为u32(无符号 32 位整数) - 函数按 module 作用域,即两个 module 中同名函数不会冲突
- module 作用域适用于所有类型(例如
mod a { struct foo; }中的struct foo与mod b { struct foo; }中的b::foo是不同类型)
- module 作用域适用于所有类型(例如
- 如前所述,函数用
Starter code — 补全函数:
mod math {
// TODO: implement pub fn add(a: u32, b: u32) -> u32
}
fn greet(name: &str) -> String {
// TODO: return "Hello, <name>! The secret number is <math::add(21,21)>"
todo!()
}
fn main() {
println!("{}", greet("Rustacean"));
}
Solution (click to expand)
mod math {
pub fn add(a: u32, b: u32) -> u32 {
a + b
}
}
fn greet(name: &str) -> String {
format!("Hello, {}! The secret number is {}", name, math::add(21, 21))
}
fn main() {
println!("{}", greet("Rustacean"));
}
// Output: Hello, Rustacean! The secret number is 42
Workspace 与 crate(包)
- 任何有意义的 Rust 项目应用 workspace 组织组件 crate
- workspace 是用于构建目标二进制的本地 crate 集合。workspace 根目录的
Cargo.toml应指向各成员包(crate)
- workspace 是用于构建目标二进制的本地 crate 集合。workspace 根目录的
[workspace]
resolver = "2"
members = ["package1", "package2"]
workspace_root/
|-- Cargo.toml # Workspace configuration
|-- package1/
| |-- Cargo.toml # Package 1 configuration
| `-- src/
| `-- lib.rs # Package 1 source code
|-- package2/
| |-- Cargo.toml # Package 2 configuration
| `-- src/
| `-- main.rs # Package 2 source code
练习:使用 workspace 与包依赖
- 创建简单包并在
hello world程序中使用 - 创建 workspace 目录
mkdir workspace
cd workspace
- 创建 Cargo.toml 并添加以下内容,创建空 workspace
[workspace]
resolver = "2"
members = []
- 添加包(
cargo new --lib指定库而非可执行文件)
cargo new hello
cargo new --lib hellolib
练习:使用 workspace 与包依赖
- 查看
hello与hellolib生成的 Cargo.toml。注意两者都已加入上层Cargo.toml hellolib中存在lib.rs表示库包(定制选项见 https://doc.rust-lang.org/cargo/reference/cargo-targets.html)- 在
hello的Cargo.toml中添加对hellolib的依赖
[dependencies]
hellolib = {path = "../hellolib"}
- 使用
hellolib的add()
fn main() {
println!("Hello, world! {}", hellolib::add(21, 21));
}
Solution (click to expand)
The complete workspace setup:
# Terminal commands
mkdir workspace && cd workspace
# Create workspace Cargo.toml
cat > Cargo.toml << 'EOF'
[workspace]
resolver = "2"
members = ["hello", "hellolib"]
EOF
cargo new hello
cargo new --lib hellolib
# hello/Cargo.toml — add dependency
[dependencies]
hellolib = {path = "../hellolib"}
#![allow(unused)]
fn main() {
// hellolib/src/lib.rs — already has add() from cargo new --lib
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
}
// hello/src/main.rs
fn main() {
println!("Hello, world! {}", hellolib::add(21, 21));
}
// Output: Hello, world! 42
使用 crates.io 社区 crate
- Rust 有活跃的社区 crate 生态(见 https://crates.io/)
- Rust 哲学是保持标准库精简,将功能外包给社区 crate
- 使用社区 crate 无硬性规则,但经验法则是确保 crate 成熟度合理(版本号可反映)且仍在维护。若有疑问可咨询内部资源
crates.io上每个 crate 有主版本与次版本- crate 应遵循此处定义的
SemVer主/次版本指南:https://doc.rust-lang.org/cargo/reference/semver.html - 简言之:同一 minor 版本内不应有破坏性变更。例如 v0.11 须与 v0.15 兼容(v0.20 可有破坏性变更)
- crate 应遵循此处定义的
Crate 依赖与 SemVer
- crate 可依赖特定版本、特定 minor/major 版本,或不限制。下列示例为在
Cargo.toml中声明对randcrate 的依赖 - 至少
0.10.0,但< 0.11.0的任意版本均可
[dependencies]
rand = { version = "0.10.0"}
- 仅
0.10.0,不接受其他版本
[dependencies]
rand = { version = "=0.10.0"}
- 不限制;
cargo将选择最新版本
[dependencies]
rand = { version = "*"}
- 参考:https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
练习:使用 rand crate
- 修改
helloworld示例以打印随机数 - 用
cargo add rand添加依赖 - 以
https://docs.rs/rand/latest/rand/为 API 参考
Starter code — 运行 cargo add rand 后在 main.rs 中添加:
use rand::RngExt;
fn main() {
let mut rng = rand::rng();
// TODO: Generate and print a random u32 in 1..=100
// TODO: Generate and print a random bool
// TODO: Generate and print a random f64
}
Solution (click to expand)
use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let n: u32 = rng.random_range(1..=100);
println!("Random number (1-100): {n}");
// Generate a random boolean
let b: bool = rng.random();
println!("Random bool: {b}");
// Generate a random float between 0.0 and 1.0
let f: f64 = rng.random();
println!("Random float: {f:.4}");
}
Cargo.toml 与 Cargo.lock
- 如前所述,Cargo.lock 由 Cargo.toml 自动生成
- Cargo.lock 的主要目的是保证可重现构建。例如若
Cargo.toml指定0.10.0,cargo 可选< 0.11.0的任意版本 - Cargo.lock 包含构建时使用的 rand crate 具体版本
- 建议将
Cargo.lock纳入 git 仓库以保证可重现构建
- Cargo.lock 的主要目的是保证可重现构建。例如若
Cargo test 功能
- Rust 单元测试通常与源码同文件(按惯例),常分组为独立 module
- 测试代码从不纳入实际二进制。这由
cfg(配置)特性实现。配置可用于平台特定代码(如LinuxvsWindows) - 测试用
cargo test执行。参考:https://doc.rust-lang.org/reference/conditional-compilation.html
- 测试代码从不纳入实际二进制。这由
#![allow(unused)]
fn main() {
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
// Will be included only during testing
#[cfg(test)]
mod tests {
use super::*; // This makes all types in the parent scope visible
#[test]
fn it_works() {
let result = add(2, 2); // Alternatively, super::add(2, 2);
assert_eq!(result, 4);
}
}
}
其他 Cargo 功能
cargo还有其他实用功能,包括:cargo clippy是优秀的 Rust 代码 lint 工具。一般应修复警告(或确有理由时极少抑制)cargo format运行rustfmt格式化源码。保证入库代码格式统一,终结风格争论cargo doc可从///风格注释生成文档。crates.io上所有 crate 文档均用此方法生成
构建配置:控制优化
C 中向 gcc/clang 传 -O0、-O2、-Os、-flto。Rust 在 Cargo.toml 中配置构建配置:
# Cargo.toml — build profile configuration
[profile.dev]
opt-level = 0 # No optimization (fast compile, like -O0)
debug = true # Full debug symbols (like -g)
[profile.release]
opt-level = 3 # Maximum optimization (like -O3)
lto = "fat" # Link-Time Optimization (like -flto)
strip = true # Strip symbols (like the strip command)
codegen-units = 1 # Single codegen unit — slower compile, better optimization
panic = "abort" # No unwind tables (smaller binary)
| C/GCC 标志 | Cargo.toml 键 | 取值 |
|---|---|---|
-O0 / -O2 / -O3 | opt-level | 0、1、2、3、"s"、"z" |
-flto | lto | false、"thin"、"fat" |
-g / 无 -g | debug | true、false、"line-tables-only" |
strip 命令 | strip | "none"、"debuginfo"、"symbols"、true/false |
| — | codegen-units | 1 = 最优优化、编译最慢 |
cargo build # Uses [profile.dev]
cargo build --release # Uses [profile.release]
构建脚本(build.rs):链接 C 库
C 中用 Makefile 或 CMake 链接库并运行代码生成。
Rust 在 crate 根目录使用 build.rs:
// build.rs — runs before compiling the crate
fn main() {
// Link a system C library (like -lbmc_ipmi in gcc)
println!("cargo::rustc-link-lib=bmc_ipmi");
// Where to find the library (like -L/usr/lib/bmc)
println!("cargo::rustc-link-search=/usr/lib/bmc");
// Re-run if the C header changes
println!("cargo::rerun-if-changed=wrapper.h");
}
甚至可从 Rust crate 直接编译 C 源文件:
# Cargo.toml
[build-dependencies]
cc = "1" # C compiler integration
// build.rs
fn main() {
cc::Build::new()
.file("src/c_helpers/ipmi_raw.c")
.include("/usr/include/bmc")
.compile("ipmi_raw"); // Produces libipmi_raw.a, linked automatically
println!("cargo::rerun-if-changed=src/c_helpers/ipmi_raw.c");
}
| C / Make / CMake | Rust build.rs |
|---|---|
-lfoo | println!("cargo::rustc-link-lib=foo") |
-L/path | println!("cargo::rustc-link-search=/path") |
| 编译 C 源 | cc::Build::new().file("foo.c").compile("foo") |
| 生成代码 | 写入 $OUT_DIR,再 include!() |
交叉编译
C 中交叉编译需安装独立工具链(arm-linux-gnueabihf-gcc)并配置 Make/CMake。Rust 中:
# Install a cross-compilation target
rustup target add aarch64-unknown-linux-gnu
# Cross-compile
cargo build --target aarch64-unknown-linux-gnu --release
在 .cargo/config.toml 中指定链接器:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
| C 交叉编译 | Rust 等价 |
|---|---|
apt install gcc-aarch64-linux-gnu | rustup target add aarch64-unknown-linux-gnu + 安装链接器 |
CC=aarch64-linux-gnu-gcc make | .cargo/config.toml [target.X] linker = "..." |
#ifdef __aarch64__ | #[cfg(target_arch = "aarch64")] |
| 独立 Makefile 目标 | cargo build --target ... |
特性标志:条件编译
C 用 #ifdef 与 -DFOO 做条件编译。Rust 在 Cargo.toml 中定义特性标志:
# Cargo.toml
[features]
default = ["json"] # Enabled by default
json = ["dep:serde_json"] # Optional dependency
verbose = [] # Flag with no dependency
gpu = ["dep:cuda-sys"] # Optional GPU support
#![allow(unused)]
fn main() {
// Code gated on features:
#[cfg(feature = "json")]
pub fn parse_config(data: &str) -> Result<Config, Error> {
serde_json::from_str(data).map_err(Error::from)
}
#[cfg(feature = "verbose")]
macro_rules! verbose {
($($arg:tt)*) => { eprintln!("[VERBOSE] {}", format!($($arg)*)); }
}
#[cfg(not(feature = "verbose"))]
macro_rules! verbose {
($($arg:tt)*) => {}; // Compiles to nothing
}
}
| C 预处理器 | Rust 特性标志 |
|---|---|
gcc -DDEBUG | cargo build --features verbose |
#ifdef DEBUG | #[cfg(feature = "verbose")] |
#define MAX 100 | const MAX: u32 = 100; |
#ifdef __linux__ | #[cfg(target_os = "linux")] |
集成测试 vs 单元测试
单元测试与代码同处,使用 #[cfg(test)]。集成测试位于 tests/,仅测试 crate 的公开 API:
#![allow(unused)]
fn main() {
// tests/smoke_test.rs — no #[cfg(test)] needed
use my_crate::parse_config;
#[test]
fn parse_valid_config() {
let config = parse_config("test_data/valid.json").unwrap();
assert_eq!(config.max_retries, 5);
}
}
| 方面 | 单元测试(#[cfg(test)]) | 集成测试(tests/) |
|---|---|---|
| 位置 | 与代码同文件 | 独立 tests/ 目录 |
| 访问 | 私有 + 公开项 | 仅公开 API |
| 运行命令 | cargo test | cargo test --test smoke_test |
测试模式与策略
C 固件团队常用 CUnit、CMocka 或自定义框架,样板很多。Rust 内置测试 harness 能力更强。本节涵盖生产代码所需模式。
#[should_panic] — 测试预期失败
#![allow(unused)]
fn main() {
// Test that certain conditions cause panics (like C's assert failures)
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_bounds_check() {
let v = vec![1, 2, 3];
let _ = v[10]; // Should panic
}
#[test]
#[should_panic(expected = "temperature exceeds safe limit")]
fn test_thermal_shutdown() {
fn check_temperature(celsius: f64) {
if celsius > 105.0 {
panic!("temperature exceeds safe limit: {celsius}°C");
}
}
check_temperature(110.0);
}
}
#[ignore] — 慢速或依赖硬件的测试
#![allow(unused)]
fn main() {
// Mark tests that require special conditions (like C's #ifdef HARDWARE_TEST)
#[test]
#[ignore = "requires GPU hardware"]
fn test_gpu_ecc_scrub() {
// This test only runs on machines with GPUs
// Run with: cargo test -- --ignored
// Run with: cargo test -- --include-ignored (runs ALL tests)
}
}
返回 Result 的测试(替代 unwrap 链)
#![allow(unused)]
fn main() {
// Instead of many unwrap() calls that hide the actual failure:
#[test]
fn test_config_parsing() -> Result<(), Box<dyn std::error::Error>> {
let json = r#"{"hostname": "node-01", "port": 8080}"#;
let config: ServerConfig = serde_json::from_str(json)?; // ? instead of unwrap()
assert_eq!(config.hostname, "node-01");
assert_eq!(config.port, 8080);
Ok(()) // Test passes if we reach here without error
}
}
用构建函数做测试夹具
C 用 setUp()/tearDown()。Rust 用辅助函数与 Drop:
#![allow(unused)]
fn main() {
struct TestFixture {
temp_dir: std::path::PathBuf,
config: Config,
}
impl TestFixture {
fn new() -> Self {
let temp_dir = std::env::temp_dir().join(format!("test_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir).unwrap();
let config = Config {
log_dir: temp_dir.clone(),
max_retries: 3,
..Default::default()
};
Self { temp_dir, config }
}
}
impl Drop for TestFixture {
fn drop(&mut self) {
// Automatic cleanup — like C's tearDown() but can't be forgotten
let _ = std::fs::remove_dir_all(&self.temp_dir);
}
}
#[test]
fn test_with_fixture() {
let fixture = TestFixture::new();
// Use fixture.config, fixture.temp_dir...
assert!(fixture.temp_dir.exists());
// fixture is automatically dropped here → cleanup runs
}
}
用 Trait 模拟硬件接口
C 中模拟硬件需预处理器技巧或函数指针替换。 Rust 中 Trait 很自然:
#![allow(unused)]
fn main() {
// Production trait for IPMI communication
trait IpmiTransport {
fn send_command(&self, cmd: u8, data: &[u8]) -> Result<Vec<u8>, String>;
}
// Real implementation (used in production)
struct RealIpmi { /* BMC connection details */ }
impl IpmiTransport for RealIpmi {
fn send_command(&self, cmd: u8, data: &[u8]) -> Result<Vec<u8>, String> {
// Actually talks to BMC hardware
todo!("Real IPMI call")
}
}
// Mock implementation (used in tests)
struct MockIpmi {
responses: std::collections::HashMap<u8, Vec<u8>>,
}
impl IpmiTransport for MockIpmi {
fn send_command(&self, cmd: u8, _data: &[u8]) -> Result<Vec<u8>, String> {
self.responses.get(&cmd)
.cloned()
.ok_or_else(|| format!("No mock response for cmd 0x{cmd:02x}"))
}
}
// Generic function that works with both real and mock
fn read_sensor_temperature(transport: &dyn IpmiTransport) -> Result<f64, String> {
let response = transport.send_command(0x2D, &[])?;
if response.len() < 2 {
return Err("Response too short".into());
}
Ok(response[0] as f64 + (response[1] as f64 / 256.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_temperature_reading() {
let mut mock = MockIpmi { responses: std::collections::HashMap::new() };
mock.responses.insert(0x2D, vec![72, 128]); // 72.5°C
let temp = read_sensor_temperature(&mock).unwrap();
assert!((temp - 72.5).abs() < 0.01);
}
#[test]
fn test_short_response() {
let mock = MockIpmi { responses: std::collections::HashMap::new() };
// No response configured → error
assert!(read_sensor_temperature(&mock).is_err());
}
}
}
用 proptest 做基于属性的测试
不测试具体值,而测试对所有输入都成立的不变式:
#![allow(unused)]
fn main() {
// Cargo.toml: [dev-dependencies] proptest = "1"
use proptest::prelude::*;
fn parse_sensor_id(s: &str) -> Option<u32> {
s.strip_prefix("sensor_")?.parse().ok()
}
fn format_sensor_id(id: u32) -> String {
format!("sensor_{id}")
}
proptest! {
#[test]
fn roundtrip_sensor_id(id in 0u32..10000) {
// Property: format then parse should give back the original
let formatted = format_sensor_id(id);
let parsed = parse_sensor_id(&formatted);
prop_assert_eq!(parsed, Some(id));
}
#[test]
fn parse_rejects_garbage(s in "[^s].*") {
// Property: strings not starting with 's' should never parse
let result = parse_sensor_id(&s);
prop_assert!(result.is_none());
}
}
}
C vs Rust 测试对照
| C 测试 | Rust 等价 |
|---|---|
CUnit、CMocka、自定义框架 | 内置 #[test] + cargo test |
setUp() / tearDown() | 构建函数 + Drop Trait |
#ifdef TEST 模拟函数 | 基于 Trait 的依赖注入 |
assert(x == y) | assert_eq!(x, y),自动 diff 输出 |
| 独立测试可执行文件 | 同一二进制,#[cfg(test)] 条件编译 |
valgrind --leak-check=full ./test | cargo test(默认内存安全)+ cargo miri test |
代码覆盖率:gcov / lcov | cargo tarpaulin 或 cargo llvm-cov |
| 测试发现:手动注册 | 自动——任意 #[test] 函数都会被发现 |
测试模式
面向 C++ 程序员的测试模式
你将学到: Rust 内置测试框架——
#[test]、#[should_panic]、返回Result的测试、测试数据的构建器模式、基于 Trait 的 mock、proptest属性测试、insta快照测试,以及集成测试组织。零配置测试,替代 Google Test + CMake。
C++ 测试通常依赖外部框架(Google Test、Catch2、Boost.Test)与复杂构建集成。Rust 测试框架内置于语言与工具链——无依赖、无 CMake 集成、无测试运行器配置。
#[test] 之外的测试属性
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_pass() {
assert_eq!(2 + 2, 4);
}
// Expect a panic — equivalent to GTest's EXPECT_DEATH
#[test]
#[should_panic]
fn out_of_bounds_panics() {
let v = vec![1, 2, 3];
let _ = v[10]; // Panics — test passes
}
// Expect a panic with a specific message substring
#[test]
#[should_panic(expected = "index out of bounds")]
fn specific_panic_message() {
let v = vec![1, 2, 3];
let _ = v[10];
}
// Tests that return Result<(), E> — use ? instead of unwrap()
#[test]
fn test_with_result() -> Result<(), String> {
let value: u32 = "42".parse().map_err(|e| format!("{e}"))?;
assert_eq!(value, 42);
Ok(())
}
// Ignore slow tests by default — run with `cargo test -- --ignored`
#[test]
#[ignore]
fn slow_integration_test() {
std::thread::sleep(std::time::Duration::from_secs(10));
}
}
}
cargo test # Run all non-ignored tests
cargo test -- --ignored # Run only ignored tests
cargo test -- --include-ignored # Run ALL tests including ignored
cargo test test_name # Run tests matching a name pattern
cargo test -- --nocapture # Show println! output during tests
cargo test -- --test-threads=1 # Run tests serially (for shared state)
测试辅助:测试数据的构建器模式
C++ 中用 Google Test fixture(class MyTest : public ::testing::Test)。
Rust 中用构建函数或 Default Trait:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
// Builder function — creates test data with sensible defaults
fn make_gpu_event(severity: Severity, fault_code: u32) -> DiagEvent {
DiagEvent {
source: "accel_diag".to_string(),
severity,
message: format!("Test event FC:{fault_code}"),
fault_code,
}
}
// Reusable test fixture — a set of pre-built events
fn sample_events() -> Vec<DiagEvent> {
vec![
make_gpu_event(Severity::Critical, 67956),
make_gpu_event(Severity::Warning, 32709),
make_gpu_event(Severity::Info, 10001),
]
}
#[test]
fn filter_critical_events() {
let events = sample_events();
let critical: Vec<_> = events.iter()
.filter(|e| e.severity == Severity::Critical)
.collect();
assert_eq!(critical.len(), 1);
assert_eq!(critical[0].fault_code, 67956);
}
}
}
用 Trait 做 mock
C++ 中 mock 需 Google Mock 或手动虚函数覆盖。 Rust 中为依赖定义 Trait,在测试中替换实现:
#![allow(unused)]
fn main() {
// Production trait
trait SensorReader {
fn read_temperature(&self, sensor_id: u32) -> Result<f64, String>;
}
// Production implementation
struct HwSensorReader;
impl SensorReader for HwSensorReader {
fn read_temperature(&self, sensor_id: u32) -> Result<f64, String> {
// Real hardware call...
Ok(72.5)
}
}
// Test mock — returns predictable values
#[cfg(test)]
struct MockSensorReader {
temperatures: std::collections::HashMap<u32, f64>,
}
#[cfg(test)]
impl SensorReader for MockSensorReader {
fn read_temperature(&self, sensor_id: u32) -> Result<f64, String> {
self.temperatures.get(&sensor_id)
.copied()
.ok_or_else(|| format!("Unknown sensor {sensor_id}"))
}
}
// Function under test — generic over the reader
fn check_overtemp(reader: &impl SensorReader, ids: &[u32], threshold: f64) -> Vec<u32> {
ids.iter()
.filter(|&&id| reader.read_temperature(id).unwrap_or(0.0) > threshold)
.copied()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_overtemp_sensors() {
let mut mock = MockSensorReader { temperatures: Default::default() };
mock.temperatures.insert(0, 72.5);
mock.temperatures.insert(1, 91.0); // Over threshold
mock.temperatures.insert(2, 65.0);
let hot = check_overtemp(&mock, &[0, 1, 2], 80.0);
assert_eq!(hot, vec![1]);
}
}
}
测试中的临时文件与目录
C++ 测试常用平台相关临时目录。Rust 有 tempfile:
#![allow(unused)]
fn main() {
// Cargo.toml: [dev-dependencies]
// tempfile = "3"
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
use std::io::Write;
#[test]
fn parse_config_from_file() -> Result<(), Box<dyn std::error::Error>> {
// Create a temp file that's auto-deleted when dropped
let mut file = NamedTempFile::new()?;
writeln!(file, r#"{{"sku": "ServerNode", "level": "Quick"}}"#)?;
let config = load_config(file.path().to_str().unwrap())?;
assert_eq!(config.sku, "ServerNode");
Ok(())
// file is deleted here — no cleanup code needed
}
}
}
用 proptest 做基于属性的测试
不写具体用例,而描述对所有输入都应成立的不变式。
proptest 生成随机输入并找最小失败用例:
#![allow(unused)]
fn main() {
// Cargo.toml: [dev-dependencies]
// proptest = "1"
#[cfg(test)]
mod tests {
use proptest::prelude::*;
fn parse_and_format(n: u32) -> String {
format!("{n}")
}
proptest! {
#[test]
fn roundtrip_u32(n: u32) {
let formatted = parse_and_format(n);
let parsed: u32 = formatted.parse().unwrap();
prop_assert_eq!(n, parsed);
}
#[test]
fn string_contains_no_null(s in "[a-zA-Z0-9 ]{0,100}") {
prop_assert!(!s.contains('\0'));
}
}
}
}
用 insta 做快照测试
对产生复杂输出(JSON、格式化字符串)的测试,insta 自动生成并管理参考快照:
#![allow(unused)]
fn main() {
// Cargo.toml: [dev-dependencies]
// insta = { version = "1", features = ["json"] }
#[cfg(test)]
mod tests {
use insta::assert_json_snapshot;
#[test]
fn der_entry_format() {
let entry = DerEntry {
fault_code: 67956,
component: "GPU".to_string(),
message: "ECC error detected".to_string(),
};
// First run: creates a snapshot file in tests/snapshots/
// Subsequent runs: compares against the saved snapshot
assert_json_snapshot!(entry);
}
}
}
cargo insta test # Run tests and review new/changed snapshots
cargo insta review # Interactive review of snapshot changes
C++ vs Rust 测试对照
| C++(Google Test) | Rust | 说明 |
|---|---|---|
TEST(Suite, Name) { } | #[test] fn name() { } | 无需 suite/类层次 |
ASSERT_EQ(a, b) | assert_eq!(a, b) | 内置宏,无需框架 |
ASSERT_NEAR(a, b, eps) | assert!((a - b).abs() < eps) | 或使用 approx crate |
EXPECT_THROW(expr, type) | #[should_panic(expected = "...")] | 或 catch_unwind 精细控制 |
EXPECT_DEATH(expr, "msg") | #[should_panic(expected = "msg")] | |
class Fixture : public ::testing::Test | 构建函数 + Default | 无需继承 |
Google Mock MOCK_METHOD | Trait + 测试 impl | 更显式,无宏魔法 |
INSTANTIATE_TEST_SUITE_P(参数化) | proptest! 或宏生成测试 | |
SetUp() / TearDown() | 通过 Drop 的 RAII——清理自动 | 测试结束变量 drop |
| 独立测试二进制 + CMake | cargo test——零配置 | |
ctest --output-on-failure | cargo test -- --nocapture |
集成测试:tests/ 目录
单元测试在 #[cfg(test)] module 中与代码同处。集成测试位于 crate 根目录独立 tests/,像外部使用者一样测试库公开 API:
my_crate/
├── src/
│ └── lib.rs # Your library code
├── tests/
│ ├── smoke.rs # Each .rs file is a separate test binary
│ ├── regression.rs
│ └── common/
│ └── mod.rs # Shared test helpers (NOT a test itself)
└── Cargo.toml
#![allow(unused)]
fn main() {
// tests/smoke.rs — tests your crate as an external user would
use my_crate::DiagEngine; // Only public API is accessible
#[test]
fn engine_starts_successfully() {
let engine = DiagEngine::new("test_config.json");
assert!(engine.is_ok());
}
#[test]
fn engine_rejects_invalid_config() {
let engine = DiagEngine::new("nonexistent.json");
assert!(engine.is_err());
}
}
#![allow(unused)]
fn main() {
// tests/common/mod.rs — shared helpers, NOT compiled as a test binary
pub fn setup_test_environment() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("config.json"), r#"{"log_level": "debug"}"#).unwrap();
dir
}
}
#![allow(unused)]
fn main() {
// tests/regression.rs — can use shared helpers
mod common;
#[test]
fn regression_issue_42() {
let env = common::setup_test_environment();
let engine = my_crate::DiagEngine::new(
env.path().join("config.json").to_str().unwrap()
);
assert!(engine.is_ok());
}
}
运行集成测试:
cargo test # Runs unit AND integration tests
cargo test --test smoke # Run only tests/smoke.rs
cargo test --test regression # Run only tests/regression.rs
cargo test --lib # Run ONLY unit tests (skip integration)
与单元测试的关键区别:集成测试不能访问私有函数或
pub(crate)项。这迫使你验证公开 API 是否足够——是有价值的设计信号。C++ 术语下,类似只测公开头文件、无friend访问。
9. 错误处理
将枚举与 Option、Result 联系起来
你将学到: Rust 如何用
Option<T>替代空指针、用Result<T, E>替代异常,以及?运算符如何简洁传播错误。这是 Rust 最具特色的模式——错误是值,不是隐藏的控制流。
- 还记得前面学的
enum吗?Rust 的Option与Result就是标准库中定义的枚举:
#![allow(unused)]
fn main() {
// This is literally how Option is defined in std:
enum Option<T> {
Some(T), // Contains a value
None, // No value
}
// And Result:
enum Result<T, E> {
Ok(T), // Success with value
Err(E), // Error with details
}
}
- 因此前面学的
match模式匹配可直接用于Option与Result - Rust 没有空指针——
Option<T>是替代,编译器强制处理None情况
C++ 对照:异常 vs Result
| C++ 模式 | Rust 等价 | 优势 |
|---|---|---|
throw std::runtime_error(msg) | Err(MyError::Runtime(msg)) | 错误在返回类型中——不能忘记处理 |
try { } catch (...) { } | match result { Ok(v) => ..., Err(e) => ... } | 无隐藏控制流 |
std::optional<T> | Option<T> | 须穷尽 match——不能忘记 None |
noexcept 标注 | 默认——所有 Rust 函数都是「noexcept」 | 不存在异常 |
errno / 返回码 | Result<T, E> | 类型安全,不能忽略 |
Rust Option 类型
- Rust
Option是只有两个变体的enum:Some<T>与None- 表示
nullable类型:要么含该类型的有效值(Some<T>),要么无有效值(None) Option用于操作要么成功返回有效值、要么失败但具体错误无关的 API。例如解析字符串为整数
- 表示
fn main() {
// Returns Option<usize>
let a = "1234".find("1");
match a {
Some(a) => println!("Found 1 at index {a}"),
None => println!("Couldn't find 1")
}
}
Rust Option 类型
- Rust
Option有多种处理方式unwrap()在Option<T>为None时 panic,否则返回T,是最不首选的方式or()可返回替代值if let可测试Some<T>
生产实践: 参见 用 unwrap_or 安全取值 与 函数式变换:map、map_err、find_map,了解生产 Rust 代码中的实例。
fn main() {
// This return an Option<usize>
let a = "1234".find("1");
println!("{a:?} {}", a.unwrap());
let a = "1234".find("5").or(Some(42));
println!("{a:?}");
if let Some(a) = "1234".find("1") {
println!("{a}");
} else {
println!("Not found in string");
}
// This will panic
// "1234".find("5").unwrap();
}
Rust Result 类型
- Result 是类似
Option的enum,两个变体:Ok<T>或Err<E>Result广泛用于可能失败的 Rust API。成功时返回Ok<T>,失败时返回具体错误Err<E>
use std::num::ParseIntError;
fn main() {
let a : Result<i32, ParseIntError> = "1234z".parse();
match a {
Ok(n) => println!("Parsed {n}"),
Err(e) => println!("Parsing failed {e:?}"),
}
let a : Result<i32, ParseIntError> = "1234z".parse().or(Ok(-1));
println!("{a:?}");
if let Ok(a) = "1234".parse::<i32>() {
println!("Let OK {a}");
}
// This will panic
//"1234z".parse().unwrap();
}
Option 与 Result:同一枚硬币的两面
Option 与 Result 密切相关——Option<T> 本质上是 Result<T, ()>(错误不带信息的结果):
Option<T> | Result<T, E> | 含义 |
|---|---|---|
Some(value) | Ok(value) | 成功——有值 |
None | Err(error) | 失败——无值(Option)或错误详情(Result) |
相互转换:
fn main() {
let opt: Option<i32> = Some(42);
let res: Result<i32, &str> = opt.ok_or("value was None"); // Option → Result
let res: Result<i32, &str> = Ok(42);
let opt: Option<i32> = res.ok(); // Result → Option (discards error)
// They share many of the same methods:
// .map(), .and_then(), .unwrap_or(), .unwrap_or_else(), .is_some()/is_ok()
}
经验法则:缺失是正常情况时用
Option(如查键)。失败需要解释时用Result(如文件 I/O、解析)。
练习:用 Option 实现 log() 函数
🟢 入门
- 实现
log(),接受Option<&str>参数。参数为None时打印默认字符串 - 函数返回
Result,成功与错误类型均为()(本例永不产生错误)
Solution (click to expand)
fn log(message: Option<&str>) -> Result<(), ()> {
match message {
Some(msg) => println!("LOG: {msg}"),
None => println!("LOG: (no message provided)"),
}
Ok(())
}
fn main() {
let _ = log(Some("System initialized"));
let _ = log(None);
// Alternative using unwrap_or:
let msg: Option<&str> = None;
println!("LOG: {}", msg.unwrap_or("(default message)"));
}
// Output:
// LOG: System initialized
// LOG: (no message provided)
// LOG: (default message)
Rust 错误处理
- Rust 错误可分为不可恢复(致命)与可恢复。致命错误导致
panic- 一般应避免导致
panic的情况。panic由程序 bug 引起,包括越界索引、对Option<None>调用unwrap()等 - 对理论上不可能的条件显式
panic是可以的。可用panic!或assert!做健全性检查
- 一般应避免导致
fn main() {
let x : Option<u32> = None;
// println!("{x}", x.unwrap()); // Will panic
println!("{}", x.unwrap_or(0)); // OK -- prints 0
let x = 41;
//assert!(x == 42); // Will panic
//panic!("Something went wrong"); // Unconditional panic
let _a = vec![0, 1];
// println!("{}", a[2]); // Out of bounds panic; use a.get(2) which will return Option<T>
}
错误处理:C++ vs Rust
C++ 基于异常的错误处理问题
// C++ error handling - exceptions create hidden control flow
#include <fstream>
#include <stdexcept>
std::string read_config(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
throw std::runtime_error("Cannot open: " + path);
}
std::string content;
// What if getline throws? Is file properly closed?
// With RAII yes, but what about other resources?
std::getline(file, content);
return content; // What if caller doesn't try/catch?
}
int main() {
// ERROR: Forgot to wrap in try/catch!
auto config = read_config("nonexistent.txt");
// Exception propagates silently, program crashes
// Nothing in the function signature warned us
return 0;
}
graph TD
subgraph "C++ Error Handling Issues"
CF["Function Call"]
CR["throw exception<br/>or return code"]
CIGNORE["[ERROR] Exception not caught<br/>or return code ignored"]
CCHECK["try/catch or check"]
CERROR["Hidden control flow<br/>throws not in signature"]
CERRNO["No compile-time<br/>enforcement"]
CF --> CR
CR --> CIGNORE
CR --> CCHECK
CCHECK --> CERROR
CERROR --> CERRNO
CPROBLEMS["[ERROR] Exceptions invisible in types<br/>[ERROR] Hidden control flow<br/>[ERROR] Easy to forget try/catch<br/>[ERROR] Exception safety is hard<br/>[ERROR] noexcept is opt-in"]
end
subgraph "Rust Result<T, E> System"
RF["Function Call"]
RR["Result<T, E><br/>Ok(value) | Err(error)"]
RMUST["[OK] Must handle<br/>Compile error if ignored"]
RMATCH["Pattern matching<br/>match, if let, ?"]
RDETAIL["Detailed error info<br/>Custom error types"]
RSAFE["Type-safe<br/>No global state"]
RF --> RR
RR --> RMUST
RMUST --> RMATCH
RMATCH --> RDETAIL
RDETAIL --> RSAFE
RBENEFITS["[OK] Forced error handling<br/>[OK] Type-safe errors<br/>[OK] Detailed error info<br/>[OK] Composable with ?<br/>[OK] Zero runtime cost"]
end
style CPROBLEMS fill:#ff6b6b,color:#000
style RBENEFITS fill:#91e5a3,color:#000
style CIGNORE fill:#ff6b6b,color:#000
style RMUST fill:#91e5a3,color:#000
Result<T, E> 可视化
// Rust error handling - comprehensive and forced
use std::fs::File;
use std::io::Read;
fn read_file_content(filename: &str) -> Result<String, std::io::Error> {
let mut file = File::open(filename)?; // ? automatically propagates errors
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents) // Success case
}
fn main() {
match read_file_content("example.txt") {
Ok(content) => println!("File content: {}", content),
Err(error) => println!("Failed to read file: {}", error),
// Compiler forces us to handle both cases!
}
}
graph TD
subgraph "Result<T, E> Flow"
START["Function starts"]
OP1["File::open()"]
CHECK1{{"Result check"}}
OP2["file.read_to_string()"]
CHECK2{{"Result check"}}
SUCCESS["Ok(contents)"]
ERROR1["Err(io::Error)"]
ERROR2["Err(io::Error)"]
START --> OP1
OP1 --> CHECK1
CHECK1 -->|"Ok(file)"| OP2
CHECK1 -->|"Err(e)"| ERROR1
OP2 --> CHECK2
CHECK2 -->|"Ok(())"| SUCCESS
CHECK2 -->|"Err(e)"| ERROR2
ERROR1 --> PROPAGATE["? operator<br/>propagates error"]
ERROR2 --> PROPAGATE
PROPAGATE --> CALLER["Caller must<br/>handle error"]
end
subgraph "Pattern Matching Options"
MATCH["match result"]
IFLET["if let Ok(val) = result"]
UNWRAP["result.unwrap()<br/>[WARNING] Panics on error"]
EXPECT["result.expect(msg)<br/>[WARNING] Panics with message"]
UNWRAP_OR["result.unwrap_or(default)<br/>[OK] Safe fallback"]
QUESTION["result?<br/>[OK] Early return"]
MATCH --> SAFE1["[OK] Handles both cases"]
IFLET --> SAFE2["[OK] Handles error case"]
UNWRAP_OR --> SAFE3["[OK] Always returns value"]
QUESTION --> SAFE4["[OK] Propagates to caller"]
UNWRAP --> UNSAFE1["[ERROR] Can panic"]
EXPECT --> UNSAFE2["[ERROR] Can panic"]
end
style SUCCESS fill:#91e5a3,color:#000
style ERROR1 fill:#ffa07a,color:#000
style ERROR2 fill:#ffa07a,color:#000
style SAFE1 fill:#91e5a3,color:#000
style SAFE2 fill:#91e5a3,color:#000
style SAFE3 fill:#91e5a3,color:#000
style SAFE4 fill:#91e5a3,color:#000
style UNSAFE1 fill:#ff6b6b,color:#000
style UNSAFE2 fill:#ff6b6b,color:#000
Rust 错误处理
- Rust 用
enum Result<T, E>处理可恢复错误Ok<T>变体含成功结果,Err<E>含错误
fn main() {
let x = "1234x".parse::<u32>();
match x {
Ok(x) => println!("Parsed number {x}"),
Err(e) => println!("Parsing error {e:?}"),
}
let x = "1234".parse::<u32>();
// Same as above, but with valid number
if let Ok(x) = &x {
println!("Parsed number {x}")
} else if let Err(e) = &x {
println!("Error: {e:?}");
}
}
Rust 错误处理
- try 运算符
?是matchOk/Err模式的便捷简写- 方法须返回
Result<T, E>才能使用? Result<T, E>的类型可改。下例返回与str::parse()相同的错误类型(std::num::ParseIntError)
- 方法须返回
fn double_string_number(s : &str) -> Result<u32, std::num::ParseIntError> {
let x = s.parse::<u32>()?; // Returns immediately in case of an error
Ok(x*2)
}
fn main() {
let result = double_string_number("1234");
println!("{result:?}");
let result = double_string_number("1234x");
println!("{result:?}");
}
Rust 错误处理
- 错误可映射为其他类型或默认值(https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_default)
#![allow(unused)]
fn main() {
// Changes the error type to () in case of error
fn double_string_number(s : &str) -> Result<u32, ()> {
let x = s.parse::<u32>().map_err(|_|())?; // Returns immediately in case of an error
Ok(x*2)
}
}
#![allow(unused)]
fn main() {
fn double_string_number(s : &str) -> Result<u32, ()> {
let x = s.parse::<u32>().unwrap_or_default(); // Defaults to 0 in case of parse error
Ok(x*2)
}
}
#![allow(unused)]
fn main() {
fn double_optional_number(x : Option<u32>) -> Result<u32, ()> {
// ok_or converts Option<None> to Result<u32, ()> in the below
x.ok_or(()).map(|x|x*2) // .map() is applied only on Ok(u32)
}
}
练习:错误处理
🟡 中级
- 实现带单个 u32 参数的
log()。参数不是 42 则返回错误。成功与错误的Result<>类型均为() - 调用
log()的函数在log()返回错误时立即退出,类型同为Result<>;否则打印 log 调用成功
fn log(x: u32) -> ?? {
}
fn call_log(x: u32) -> ?? {
// Call log(x), then exit immediately if it return an error
println!("log was successfully called");
}
fn main() {
call_log(42);
call_log(43);
}
Solution (click to expand)
fn log(x: u32) -> Result<(), ()> {
if x == 42 {
Ok(())
} else {
Err(())
}
}
fn call_log(x: u32) -> Result<(), ()> {
log(x)?; // Exit immediately if log() returns an error
println!("log was successfully called with {x}");
Ok(())
}
fn main() {
let _ = call_log(42); // Prints: log was successfully called with 42
let _ = call_log(43); // Returns Err(()), nothing printed
}
// Output:
// log was successfully called with 42
Rust Option 与 Result 要点
你将学到: 惯用错误处理模式——
unwrap()的安全替代、?传播、自定义错误类型,以及生产代码中何时用anyhowvsthiserror。
Option与Result是惯用 Rust 的核心部分unwrap()的安全替代:
#![allow(unused)]
fn main() {
// Option<T> safe alternatives
let value = opt.unwrap_or(default); // Provide fallback value
let value = opt.unwrap_or_else(|| compute()); // Lazy computation for fallback
let value = opt.unwrap_or_default(); // Use Default trait implementation
let value = opt.expect("descriptive message"); // Only when panic is acceptable
// Result<T, E> safe alternatives
let value = result.unwrap_or(fallback); // Ignore error, use fallback
let value = result.unwrap_or_else(|e| handle(e)); // Handle error, return fallback
let value = result.unwrap_or_default(); // Use Default trait
}
- 用模式匹配显式控制:
#![allow(unused)]
fn main() {
match some_option {
Some(value) => println!("Got: {}", value),
None => println!("No value found"),
}
match some_result {
Ok(value) => process(value),
Err(error) => log_error(error),
}
}
- 用
?传播错误:短路并向上冒泡错误
#![allow(unused)]
fn main() {
fn process_file(path: &str) -> Result<String, std::io::Error> {
let content = std::fs::read_to_string(path)?; // Automatically returns error
Ok(content.to_uppercase())
}
}
- 变换方法:
map():变换成功值Ok(T)->Ok(U)或Some(T)->Some(U)map_err():变换错误类型Err(E)->Err(F)and_then():链接可能失败的操作
- 在自有 API 中使用:优先
Result<T, E>而非异常或错误码 - 参考:Option 文档 | Result 文档
Rust 常见陷阱与调试技巧
- 借用问题:新手最常见错误
- “cannot borrow as mutable” -> 同时只允许一个可变引用
- “borrowed value does not live long enough” -> 引用比所指数据活得更久
- 修复:用作用域
{}限制引用生命周期,或必要时 clone 数据
- 缺少 Trait 实现:“method not found” 错误
- 修复:为常见 Trait 添加
#[derive(Debug, Clone, PartialEq)] - 用
cargo check获得比cargo run更好的错误信息
- 修复:为常见 Trait 添加
- Debug 模式下整数溢出:Rust 在溢出时 panic
- 修复:用
wrapping_add()、saturating_add()或checked_add()明确行为
- 修复:用
Stringvs&str混淆:不同场景用不同类型- 字符串切片用
&str(借用),拥有字符串用String - 修复:用
.to_string()或String::from()将&str转为String
- 字符串切片用
- 与借用检查器对抗:不要试图绕过它
- 修复:重构代码以配合所有权规则,而非对抗
- 复杂共享场景可考虑
Rc<RefCell<T>>(谨慎使用)
错误处理示例:好 vs 坏
#![allow(unused)]
fn main() {
// [ERROR] BAD: Can panic unexpectedly
fn bad_config_reader() -> String {
let config = std::env::var("CONFIG_FILE").unwrap(); // Panic if not set!
std::fs::read_to_string(config).unwrap() // Panic if file missing!
}
// [OK] GOOD: Handles errors gracefully
fn good_config_reader() -> Result<String, ConfigError> {
let config_path = std::env::var("CONFIG_FILE")
.unwrap_or_else(|_| "default.conf".to_string()); // Fallback to default
let content = std::fs::read_to_string(config_path)
.map_err(ConfigError::FileRead)?; // Convert and propagate error
Ok(content)
}
// [OK] EVEN BETTER: With proper error types
use thiserror::Error;
#[derive(Error, Debug)]
enum ConfigError {
#[error("Failed to read config file: {0}")]
FileRead(#[from] std::io::Error),
#[error("Invalid configuration: {message}")]
Invalid { message: String },
}
}
下面分解发生了什么。ConfigError 只有两个变体——I/O 错误与校验错误。这是多数 module 的合适起点:
ConfigError 变体 | 持有 | 由谁创建 |
|---|---|---|
FileRead(io::Error) | 原始 I/O 错误 | #[from] 经 ? 自动转换 |
Invalid { message } | 人类可读说明 | 你的校验代码 |
现在可编写返回 Result<T, ConfigError> 的函数:
#![allow(unused)]
fn main() {
fn read_config(path: &str) -> Result<String, ConfigError> {
let content = std::fs::read_to_string(path)?; // io::Error → ConfigError::FileRead
if content.is_empty() {
return Err(ConfigError::Invalid {
message: "config file is empty".to_string(),
});
}
Ok(content)
}
}
🟢 自学检查点: 继续前请确认能回答:
- 为何
read_to_string上的?能工作?(因为#[from]生成impl From<io::Error> for ConfigError)- 若添加第三变体
MissingKey(String),需改哪些代码?(只需加变体;现有代码仍可编译)
Crate 级错误类型与 Result 别名
项目超出单文件后,会将各 module 级错误合并为 crate 级错误类型。这是生产 Rust 的标准模式。下面从上面的 ConfigError 构建。
真实 Rust 项目中,每个 crate(或重要 module)定义自己的 Error 枚举与 Result 类型别名。这是惯用模式——类似 C++ 中为每库定义异常层次与 using Result = std::expected<T, Error>。
模式
#![allow(unused)]
fn main() {
// src/error.rs (or at the top of lib.rs)
use thiserror::Error;
/// Every error this crate can produce.
#[derive(Error, Debug)]
pub enum Error {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error), // auto-converts via From
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error), // auto-converts via From
#[error("Invalid sensor id: {0}")]
InvalidSensor(u32), // domain-specific variant
#[error("Timeout after {ms} ms")]
Timeout { ms: u64 },
}
/// Crate-wide Result alias — saves typing throughout the crate.
pub type Result<T> = core::result::Result<T, Error>;
}
如何简化每个函数
无别名时需写:
#![allow(unused)]
fn main() {
// Verbose — error type repeated everywhere
fn read_sensor(id: u32) -> Result<f64, crate::Error> { ... }
fn parse_config(path: &str) -> Result<Config, crate::Error> { ... }
}
有别名后:
#![allow(unused)]
fn main() {
// Clean — just `Result<T>`
use crate::{Error, Result};
fn read_sensor(id: u32) -> Result<f64> {
if id > 128 {
return Err(Error::InvalidSensor(id));
}
let raw = std::fs::read_to_string(format!("/dev/sensor/{id}"))?; // io::Error → Error::Io
let value: f64 = raw.trim().parse()
.map_err(|_| Error::InvalidSensor(id))?;
Ok(value)
}
}
Io 上的 #[from] 属性免费生成此 impl:
#![allow(unused)]
fn main() {
// Auto-generated by thiserror's #[from]
impl From<std::io::Error> for Error {
fn from(source: std::io::Error) -> Self {
Error::Io(source)
}
}
}
这使 ? 能工作:当某函数返回 std::io::Error 而你的函数返回 Result<T>(你的别名)时,编译器自动调用 From::from() 转换。
组合 module 级错误
较大 crate 按 module 拆分错误,在 crate 根组合:
#![allow(unused)]
fn main() {
// src/config/error.rs
#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
#[error("Missing key: {0}")]
MissingKey(String),
#[error("Invalid value for '{key}': {reason}")]
InvalidValue { key: String, reason: String },
}
// src/error.rs (crate-level)
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)] // delegates Display to inner error
Config(#[from] crate::config::ConfigError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = core::result::Result<T, Error>;
}
调用方仍可匹配具体配置错误:
#![allow(unused)]
fn main() {
match result {
Err(Error::Config(ConfigError::MissingKey(k))) => eprintln!("Add '{k}' to config"),
Err(e) => eprintln!("Other error: {e}"),
Ok(v) => use_value(v),
}
}
C++ 对照
| 概念 | C++ | Rust |
|---|---|---|
| 错误层次 | class AppError : public std::runtime_error | #[derive(thiserror::Error)] enum Error { ... } |
| 返回错误 | std::expected<T, Error> 或 throw | fn foo() -> Result<T> |
| 转换错误 | 手动 try/catch + 重抛 | #[from] + ?——零样板 |
| Result 别名 | template<class T> using Result = std::expected<T, Error>; | pub type Result<T> = core::result::Result<T, Error>; |
| 错误消息 | 覆盖 what() | #[error("...")]——编译进 Display impl |
Rust Trait(特征)
你将学到: Trait——Rust 对接口、抽象基类和运算符重载的回答。你将学习如何定义 Trait、为类型实现 Trait,以及如何使用动态分发(
dyn Trait)与静态分发(泛型)。对 C++ 开发者:Trait 替代虚函数、CRTP 和 concepts。对 C 开发者:Trait 是 Rust 实现多态的结构化方式。
- Rust Trait 与其他语言中的接口类似
- Trait 定义必须由实现该 Trait 的类型所定义的方法。
fn main() {
trait Pet {
fn speak(&self);
}
struct Cat;
struct Dog;
impl Pet for Cat {
fn speak(&self) {
println!("Meow");
}
}
impl Pet for Dog {
fn speak(&self) {
println!("Woof!")
}
}
let c = Cat{};
let d = Dog{};
c.speak(); // There is no "is a" relationship between Cat and Dog
d.speak(); // There is no "is a" relationship between Cat and Dog
}
Trait 与 C++ Concepts 及接口
传统 C++ 继承与 Rust Trait
// C++ - Inheritance-based polymorphism
class Animal {
public:
virtual void speak() = 0; // Pure virtual function
virtual ~Animal() = default;
};
class Cat : public Animal { // "Cat IS-A Animal"
public:
void speak() override {
std::cout << "Meow" << std::endl;
}
};
void make_sound(Animal* animal) { // Runtime polymorphism
animal->speak(); // Virtual function call
}
#![allow(unused)]
fn main() {
// Rust - Composition over inheritance with traits
trait Animal {
fn speak(&self);
}
struct Cat; // Cat is NOT an Animal, but IMPLEMENTS Animal behavior
impl Animal for Cat { // "Cat CAN-DO Animal behavior"
fn speak(&self) {
println!("Meow");
}
}
fn make_sound<T: Animal>(animal: &T) { // Static polymorphism
animal.speak(); // Direct function call (zero cost)
}
}
graph TD
subgraph "C++ Object-Oriented Hierarchy"
CPP_ANIMAL["Animal<br/>(Abstract base class)"]
CPP_CAT["Cat : public Animal<br/>(IS-A relationship)"]
CPP_DOG["Dog : public Animal<br/>(IS-A relationship)"]
CPP_ANIMAL --> CPP_CAT
CPP_ANIMAL --> CPP_DOG
CPP_VTABLE["Virtual function table<br/>(Runtime dispatch)"]
CPP_HEAP["Often requires<br/>heap allocation"]
CPP_ISSUES["[ERROR] Deep inheritance trees<br/>[ERROR] Diamond problem<br/>[ERROR] Runtime overhead<br/>[ERROR] Tight coupling"]
end
subgraph "Rust Trait-Based Composition"
RUST_TRAIT["trait Animal<br/>(Behavior definition)"]
RUST_CAT["struct Cat<br/>(Data only)"]
RUST_DOG["struct Dog<br/>(Data only)"]
RUST_CAT -.->|"impl Animal for Cat<br/>(CAN-DO behavior)"| RUST_TRAIT
RUST_DOG -.->|"impl Animal for Dog<br/>(CAN-DO behavior)"| RUST_TRAIT
RUST_STATIC["Static dispatch<br/>(Compile-time)"]
RUST_STACK["Stack allocation<br/>possible"]
RUST_BENEFITS["[OK] No inheritance hierarchy<br/>[OK] Multiple trait impls<br/>[OK] Zero runtime cost<br/>[OK] Loose coupling"]
end
style CPP_ISSUES fill:#ff6b6b,color:#000
style RUST_BENEFITS fill:#91e5a3,color:#000
style CPP_VTABLE fill:#ffa07a,color:#000
style RUST_STATIC fill:#91e5a3,color:#000
Trait 约束与泛型约束
#![allow(unused)]
fn main() {
use std::fmt::Display;
use std::ops::Add;
// C++ template equivalent (less constrained)
// template<typename T>
// T add_and_print(T a, T b) {
// // No guarantee T supports + or printing
// return a + b; // Might fail at compile time
// }
// Rust - explicit trait bounds
fn add_and_print<T>(a: T, b: T) -> T
where
T: Display + Add<Output = T> + Copy,
{
println!("Adding {} + {}", a, b); // Display trait
a + b // Add trait
}
}
graph TD
subgraph "Generic Constraints Evolution"
UNCONSTRAINED["fn process<T>(data: T)<br/>[ERROR] T can be anything"]
SINGLE_BOUND["fn process<T: Display>(data: T)<br/>[OK] T must implement Display"]
MULTI_BOUND["fn process<T>(data: T)<br/>where T: Display + Clone + Debug<br/>[OK] Multiple requirements"]
UNCONSTRAINED --> SINGLE_BOUND
SINGLE_BOUND --> MULTI_BOUND
end
subgraph "Trait Bound Syntax"
INLINE["fn func<T: Trait>(param: T)"]
WHERE_CLAUSE["fn func<T>(param: T)<br/>where T: Trait"]
IMPL_PARAM["fn func(param: impl Trait)"]
COMPARISON["Inline: Simple cases<br/>Where: Complex bounds<br/>impl: Concise syntax"]
end
subgraph "Compile-time Magic"
GENERIC_FUNC["Generic function<br/>with trait bounds"]
TYPE_CHECK["Compiler verifies<br/>trait implementations"]
MONOMORPH["Monomorphization<br/>(Create specialized versions)"]
OPTIMIZED["Fully optimized<br/>machine code"]
GENERIC_FUNC --> TYPE_CHECK
TYPE_CHECK --> MONOMORPH
MONOMORPH --> OPTIMIZED
EXAMPLE["add_and_print::<i32><br/>add_and_print::<f64><br/>(Separate functions generated)"]
MONOMORPH --> EXAMPLE
end
style UNCONSTRAINED fill:#ff6b6b,color:#000
style SINGLE_BOUND fill:#ffa07a,color:#000
style MULTI_BOUND fill:#91e5a3,color:#000
style OPTIMIZED fill:#91e5a3,color:#000
C++ 运算符重载 → Rust std::ops Trait
在 C++ 中,通过编写具有特殊名称(operator+、operator<<、operator[] 等)的自由函数或成员函数来重载运算符。在 Rust 中,每个运算符都映射到 std::ops(或用于输出的 std::fmt)中的 Trait。你需要实现 Trait,而不是编写魔法命名的函数。
并排对比:+ 运算符
// C++: operator overloading as a member or free function
struct Vec2 {
double x, y;
Vec2 operator+(const Vec2& rhs) const {
return {x + rhs.x, y + rhs.y};
}
};
Vec2 a{1.0, 2.0}, b{3.0, 4.0};
Vec2 c = a + b; // calls a.operator+(b)
#![allow(unused)]
fn main() {
use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Vec2 { x: f64, y: f64 }
impl Add for Vec2 {
type Output = Vec2; // Associated type — the result of +
fn add(self, rhs: Vec2) -> Vec2 {
Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 3.0, y: 4.0 };
let c = a + b; // calls <Vec2 as Add>::add(a, b)
println!("{c:?}"); // Vec2 { x: 4.0, y: 6.0 }
}
与 C++ 的关键差异
| 方面 | C++ | Rust |
|---|---|---|
| 机制 | 魔法函数名(operator+) | 实现 Trait(impl Add for T) |
| 发现性 | 搜索 operator+ 或阅读头文件 | 查看 Trait 实现——IDE 支持极佳 |
| 返回类型 | 自由选择 | 由 Output 关联类型固定 |
| 接收者 | 通常接受 const T&(借用) | 默认按值接受 self(移动!) |
| 对称性 | 可写 impl operator+(int, Vec2) | 须添加 impl Add<Vec2> for i32(适用孤儿规则) |
用 << 打印 | operator<<(ostream&, T)——可为任意流重载 | impl fmt::Display for T——一种规范的 to_string 表示 |
按值 self 的陷阱
在 Rust 中,Add::add(self, rhs) 按值接受 self。对于 Copy 类型(如上文的 Vec2,它 derive 了 Copy)这没问题——编译器会复制。但对于非 Copy 类型,+ 会消耗操作数:
#![allow(unused)]
fn main() {
let s1 = String::from("hello ");
let s2 = String::from("world");
let s3 = s1 + &s2; // s1 is MOVED into s3!
// println!("{s1}"); // ❌ Compile error: value used after move
println!("{s2}"); // ✅ s2 was only borrowed (&s2)
}
这就是为什么 String + &str 可行而 &str + &str 不行——Add 只为 String + &str 实现,消耗左侧的 String 以复用其缓冲区。C++ 中没有对应物:std::string::operator+ 总是创建新字符串。
完整映射:C++ 运算符 → Rust Trait
| C++ 运算符 | Rust Trait | 说明 |
|---|---|---|
operator+ | std::ops::Add | Output 关联类型 |
operator- | std::ops::Sub | |
operator* | std::ops::Mul | 不是指针解引用——那是 Deref |
operator/ | std::ops::Div | |
operator% | std::ops::Rem | |
operator-(一元) | std::ops::Neg | |
operator! / operator~ | std::ops::Not | Rust 用 ! 表示逻辑和按位 NOT(没有 ~ 运算符) |
operator&, |, ^ | BitAnd, BitOr, BitXor | |
operator<<, >>(移位) | Shl, Shr | 不是流 I/O! |
operator+= | std::ops::AddAssign | 接受 &mut self(不是 self) |
operator[] | std::ops::Index / IndexMut | 返回 &Output / &mut Output |
operator() | Fn / FnMut / FnOnce | 闭包实现这些;不能直接 impl Fn |
operator== | PartialEq(+ Eq) | 在 std::cmp 中,不在 std::ops |
operator< | PartialOrd(+ Ord) | 在 std::cmp |
operator<<(流) | fmt::Display | println!("{}", x) |
operator<<(调试) | fmt::Debug | println!("{:?}", x) |
operator bool | 无直接等价物 | 使用 impl From<T> for bool 或 .is_empty() 等命名方法 |
operator T()(隐式转换) | 无隐式转换 | 使用 From/Into Trait(显式) |
护栏:Rust 阻止了什么
- 无隐式转换:C++ 的
operator int()可能导致静默、令人惊讶的强制转换。Rust 没有隐式转换运算符——使用From/Into并显式调用.into()。 - 不能重载
&&/||:C++ 允许(破坏短路语义!)。Rust 不允许。 - 不能重载
=:赋值始终是移动或复制,从不是用户定义的。复合赋值(+=)可通过AddAssign等重载。 - 不能重载
,:C++ 允许operator,()——最著名的 C++ 陷阱之一。Rust 不允许。 - 不能重载
&(取地址):又一个 C++ 陷阱(std::addressof的存在就是为了绕过它)。Rust 的&始终表示「借用」。 - 一致性规则:你只能为自有类型实现
Add<Foreign>,或为外部类型实现Add<YourType>——永远不能为Foreign实现Add<Foreign>。这防止跨 crate 的冲突运算符定义。
结论:在 C++ 中,运算符重载功能强大但基本不受约束——你几乎可以重载任何东西,包括逗号和取地址,隐式转换可以静默触发。Rust 通过 Trait 为算术和比较运算符提供同样的表达能力,但阻止历史上危险的重载,并强制所有转换显式进行。
Rust Trait(特征)
- Rust 允许在甚至像本例中
u32这样的内置类型上实现用户定义的 Trait。然而,Trait 或类型必须属于该 crate
trait IsSecret {
fn is_secret(&self);
}
// The IsSecret trait belongs to the crate, so we are OK
impl IsSecret for u32 {
fn is_secret(&self) {
if *self == 42 {
println!("Is secret of life");
}
}
}
fn main() {
42u32.is_secret();
43u32.is_secret();
}
Rust Trait(特征)
- Trait 支持接口继承和默认实现
trait Animal {
// Default implementation
fn is_mammal(&self) -> bool {
true
}
}
trait Feline : Animal {
// Default implementation
fn is_feline(&self) -> bool {
true
}
}
struct Cat;
// Use default implementations. Note that all traits for the supertrait must be individually implemented
impl Feline for Cat {}
impl Animal for Cat {}
fn main() {
let c = Cat{};
println!("{} {}", c.is_mammal(), c.is_feline());
}
练习:Logger Trait 实现
🟡 进阶
- 实现一个名为
log()、接受u64的Log trait- 实现两个不同的 logger
SimpleLogger和ComplexLogger,它们实现Log trait。一个应输出 “Simple logger” 及u64,另一个应输出 “Complex logger” 及u64
- 实现两个不同的 logger
Solution (click to expand)
trait Log {
fn log(&self, value: u64);
}
struct SimpleLogger;
struct ComplexLogger;
impl Log for SimpleLogger {
fn log(&self, value: u64) {
println!("Simple logger: {value}");
}
}
impl Log for ComplexLogger {
fn log(&self, value: u64) {
println!("Complex logger: {value} (hex: 0x{value:x}, binary: {value:b})");
}
}
fn main() {
let simple = SimpleLogger;
let complex = ComplexLogger;
simple.log(42);
complex.log(42);
}
// Output:
// Simple logger: 42
// Complex logger: 42 (hex: 0x2a, binary: 101010)
Rust Trait 关联类型
#[derive(Debug)]
struct Small(u32);
#[derive(Debug)]
struct Big(u32);
trait Double {
type T;
fn double(&self) -> Self::T;
}
impl Double for Small {
type T = Big;
fn double(&self) -> Self::T {
Big(self.0 * 2)
}
}
fn main() {
let a = Small(42);
println!("{:?}", a.double());
}
Rust Trait impl
impl可与 Trait 一起使用,以接受任何实现了 Trait 的类型
trait Pet {
fn speak(&self);
}
struct Dog {}
struct Cat {}
impl Pet for Dog {
fn speak(&self) {println!("Woof!")}
}
impl Pet for Cat {
fn speak(&self) {println!("Meow")}
}
fn pet_speak(p: &impl Pet) {
p.speak();
}
fn main() {
let c = Cat {};
let d = Dog {};
pet_speak(&c);
pet_speak(&d);
}
Rust Trait impl
impl也可用于返回值
trait Pet {}
struct Dog;
struct Cat;
impl Pet for Cat {}
impl Pet for Dog {}
fn cat_as_pet() -> impl Pet {
let c = Cat {};
c
}
fn dog_as_pet() -> impl Pet {
let d = Dog {};
d
}
fn main() {
let p = cat_as_pet();
let d = dog_as_pet();
}
Rust 动态 Trait
- 动态 Trait 可用于在不知道底层类型的情况下调用 Trait 功能。这称为
type erasure
trait Pet {
fn speak(&self);
}
struct Dog {}
struct Cat {x: u32}
impl Pet for Dog {
fn speak(&self) {println!("Woof!")}
}
impl Pet for Cat {
fn speak(&self) {println!("Meow")}
}
fn pet_speak(p: &dyn Pet) {
p.speak();
}
fn main() {
let c = Cat {x: 42};
let d = Dog {};
pet_speak(&c);
pet_speak(&d);
}
何时使用 enum 与 dyn Trait
这三种方式都能实现多态,但权衡不同:
| 方式 | 分发 | 性能 | 异构集合? | 何时使用 |
|---|---|---|---|---|
impl Trait / 泛型 | 静态(单态化) | 零成本——编译期内联 | 否——每个槽位只有一种具体类型 | 默认选择。函数参数、返回类型 |
dyn Trait | 动态(vtable) | 每次调用略有开销(约 1 次指针间接) | 是——Vec<Box<dyn Trait>> | 需要在集合中混合类型,或插件式可扩展性 |
enum | match | 零成本——编译期已知变体 | 是——但仅限已知变体 | 变体集合封闭且在编译期已知 |
#![allow(unused)]
fn main() {
trait Shape {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Rect { w: f64, h: f64 }
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } }
impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } }
// Static dispatch — compiler generates separate code for each type
fn print_area(s: &impl Shape) { println!("{}", s.area()); }
// Dynamic dispatch — one function, works with any Shape behind a pointer
fn print_area_dyn(s: &dyn Shape) { println!("{}", s.area()); }
// Enum — closed set, no trait needed
enum ShapeEnum { Circle(f64), Rect(f64, f64) }
impl ShapeEnum {
fn area(&self) -> f64 {
match self {
ShapeEnum::Circle(r) => std::f64::consts::PI * r * r,
ShapeEnum::Rect(w, h) => w * h,
}
}
}
}
对 C++ 开发者:
impl Trait类似 C++ 模板(单态化、零成本)。dyn Trait类似 C++ 虚函数(vtable 分发)。带match的 Rust 枚举类似std::variant配合std::visit——但穷尽匹配由编译器强制。
经验法则:从
impl Trait(静态分发)开始。仅当需要异构集合或编译期无法知道具体类型时才用dyn Trait。当你拥有所有变体时用enum。
Rust 泛型
你将学到: 泛型类型参数、单态化(零成本泛型)、Trait 约束,以及 Rust 泛型与 C++ 模板的对比——错误信息更清晰,无需 SFINAE。
- 泛型允许同一算法或数据结构在不同数据类型间复用
- 泛型参数出现在
<>内的标识符中,例如:<T>。参数可以是任何合法标识符名,但通常保持简短 - 编译器在编译期执行单态化,即为遇到的
T的每种变体生成新类型
- 泛型参数出现在
// Returns a tuple of type <T> composed of left and right of type <T>
fn pick<T>(x: u32, left: T, right: T) -> (T, T) {
if x == 42 {
(left, right)
} else {
(right, left)
}
}
fn main() {
let a = pick(42, true, false);
let b = pick(42, "hello", "world");
println!("{a:?}, {b:?}");
}
Rust 泛型
- 泛型也可应用于数据类型及其关联方法。可以为特定的
<T>特化实现(示例:f32与u32)
#[derive(Debug)] // We will discuss this later
struct Point<T> {
x : T,
y : T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point {x, y}
}
fn set_x(&mut self, x: T) {
self.x = x;
}
fn set_y(&mut self, y: T) {
self.y = y;
}
}
impl Point<f32> {
fn is_secret(&self) -> bool {
self.x == 42.0
}
}
fn main() {
let mut p = Point::new(2, 4); // i32
let q = Point::new(2.0, 4.0); // f32
p.set_x(42);
p.set_y(43);
println!("{p:?} {q:?} {}", q.is_secret());
}
练习:泛型
🟢 入门
- 修改
Point类型,使 x 和 y 使用两种不同类型(T和U)
Solution (click to expand)
#[derive(Debug)]
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn new(x: T, y: U) -> Self {
Point { x, y }
}
}
fn main() {
let p1 = Point::new(42, 3.14); // Point<i32, f64>
let p2 = Point::new("hello", true); // Point<&str, bool>
let p3 = Point::new(1u8, 1000u64); // Point<u8, u64>
println!("{p1:?}");
println!("{p2:?}");
println!("{p3:?}");
}
// Output:
// Point { x: 42, y: 3.14 }
// Point { x: "hello", y: true }
// Point { x: 1, y: 1000 }
结合 Rust Trait 与泛型
- Trait 可用于对泛型类型施加限制(约束)
- 约束可在泛型类型参数后用
:指定,或使用where。以下定义泛型函数get_area,接受任何实现了ComputeAreatrait的类型T
#![allow(unused)]
fn main() {
trait ComputeArea {
fn area(&self) -> u64;
}
fn get_area<T: ComputeArea>(t: &T) -> u64 {
t.area()
}
}
结合 Rust Trait 与泛型
- 可以有多个 Trait 约束
trait Fish {}
trait Mammal {}
struct Shark;
struct Whale;
impl Fish for Shark {}
impl Fish for Whale {}
impl Mammal for Whale {}
fn only_fish_and_mammals<T: Fish + Mammal>(_t: &T) {}
fn main() {
let w = Whale {};
only_fish_and_mammals(&w);
let _s = Shark {};
// Won't compile
only_fish_and_mammals(&_s);
}
数据类型中的 Rust Trait 约束
- Trait 约束可与泛型结合用于数据类型
- 在以下示例中,我们定义
PrintDescriptiontrait和泛型structShape,其成员受 Trait 约束
#![allow(unused)]
fn main() {
trait PrintDescription {
fn print_description(&self);
}
struct Shape<S: PrintDescription> {
shape: S,
}
// Generic Shape implementation for any type that implements PrintDescription
impl<S: PrintDescription> Shape<S> {
fn print(&self) {
self.shape.print_description();
}
}
}
练习:Trait 约束与泛型
🟡 进阶
- 实现一个带有泛型成员
cipher、且该成员实现CipherText的struct
#![allow(unused)]
fn main() {
trait CipherText {
fn encrypt(&self);
}
// TO DO
//struct Cipher<>
}
- 接下来,在
struct的impl上实现名为encrypt的方法,调用cipher上的encrypt
#![allow(unused)]
fn main() {
// TO DO
impl for Cipher<> {}
}
- 接下来,在两个名为
CipherOne和CipherTwo的结构体上实现CipherText(println()即可)。创建CipherOne和CipherTwo,并用Cipher调用它们
Solution (click to expand)
trait CipherText {
fn encrypt(&self);
}
struct Cipher<T: CipherText> {
cipher: T,
}
impl<T: CipherText> Cipher<T> {
fn encrypt(&self) {
self.cipher.encrypt();
}
}
struct CipherOne;
struct CipherTwo;
impl CipherText for CipherOne {
fn encrypt(&self) {
println!("CipherOne encryption applied");
}
}
impl CipherText for CipherTwo {
fn encrypt(&self) {
println!("CipherTwo encryption applied");
}
}
fn main() {
let c1 = Cipher { cipher: CipherOne };
let c2 = Cipher { cipher: CipherTwo };
c1.encrypt();
c2.encrypt();
}
// Output:
// CipherOne encryption applied
// CipherTwo encryption applied
Rust 类型状态模式与泛型
-
Rust 类型可在编译期强制状态机转换
- 考虑一个
Drone,假设有两种状态:Idle和Flying。在Idle状态下,唯一允许的方法是takeoff()。在Flying状态下,我们允许land()
- 考虑一个
-
一种方法是用类似以下的方式建模状态机
#![allow(unused)]
fn main() {
enum DroneState {
Idle,
Flying
}
struct Drone {x: u64, y: u64, z: u64, state: DroneState} // x, y, z are coordinates
}
- 这需要大量运行时检查来强制状态机语义——▶ 试试看 了解原因
Rust 类型状态模式泛型
- 泛型允许我们在编译期强制状态机。这需要使用名为
PhantomData<T>的特殊泛型 PhantomData<T>是zero-sized标记数据类型。此处我们用它表示Idle和Flying状态,但运行时大小为zero- 注意
takeoff和land方法将self作为参数。这称为consuming(与使用借用的&self相对)。基本上,一旦我们在Drone<Idle>上调用takeoff(),只能得到Drone<Flying>,反之亦然
#![allow(unused)]
fn main() {
struct Drone<T> {x: u64, y: u64, z: u64, state: PhantomData<T> }
impl Drone<Idle> {
fn takeoff(self) -> Drone<Flying> {...}
}
impl Drone<Flying> {
fn land(self) -> Drone<Idle> { ...}
}
}
- [▶ 在 Rust Playground 中尝试](https://play.rust-lang.org/)
Rust 类型状态模式泛型
- 要点:
- 状态可用结构体表示(零大小)
- 可将状态
T与PhantomData<T>结合(零大小) - 为状态机特定阶段实现方法现在只需
impl State<T> - 使用消耗
self的方法从一个状态转换到另一个状态 - 这给我们
zero cost抽象。编译器可在编译期强制状态机,除非状态正确否则无法调用方法
Rust 建造者模式
- 消耗
self对建造者模式很有用 - 考虑一个有数十个引脚的 GPIO 配置。引脚可配置为高或低(默认为低)
#![allow(unused)]
fn main() {
#[derive(default)]
enum PinState {
#[default]
Low,
High,
}
#[derive(default)]
struct GPIOConfig {
pin0: PinState,
pin1: PinState
...
}
}
- 建造者模式可用于链式构造 GPIO 配置——▶ 试试看
Rust From 与 Into Trait
你将学到: Rust 的类型转换 Trait——用于不会失败的转换的
From<T>和Into<T>,用于可能失败的TryFrom和TryInto。实现From即可免费获得Into。替代 C++ 转换运算符和构造函数。
From和Into是便于类型转换的互补 Trait- 类型通常实现
FromTrait。String::from("Rust")将&str转换为String。 当存在From<T> for U时,Rust 还提供Into<U> for T,因此let s: String = "Rust".into();也能工作。
struct Point {x: u32, y: u32}
// Construct a Point from a tuple
impl From<(u32, u32)> for Point {
fn from(xy : (u32, u32)) -> Self {
Point {x : xy.0, y: xy.1} // Construct Point using the tuple elements
}
}
fn main() {
let s = String::from("Rust");
let x = u32::from(true);
let p = Point::from((40, 42));
// let p : Point = (40,42).into(); // Alternate form of the above
println!("s: {s} x:{x} p.x:{} p.y {}", p.x, p.y);
}
练习:From 与 Into
- 为
Point实现FromTrait,转换为名为TransposePoint的类型。TransposePoint交换Point的x和y元素
Solution (click to expand)
struct Point { x: u32, y: u32 }
struct TransposePoint { x: u32, y: u32 }
impl From<Point> for TransposePoint {
fn from(p: Point) -> Self {
TransposePoint { x: p.y, y: p.x }
}
}
fn main() {
let p = Point { x: 10, y: 20 };
let tp = TransposePoint::from(p);
println!("Transposed: x={}, y={}", tp.x, tp.y); // x=20, y=10
// Using .into() — works automatically when From is implemented
let p2 = Point { x: 3, y: 7 };
let tp2: TransposePoint = p2.into();
println!("Transposed: x={}, y={}", tp2.x, tp2.y); // x=7, y=3
}
// Output:
// Transposed: x=20, y=10
// Transposed: x=7, y=3
Rust Default Trait
Default可用于为类型实现默认值- 类型可使用
Derive宏配合Default,或提供自定义实现
- 类型可使用
#[derive(Default, Debug)]
struct Point {x: u32, y: u32}
#[derive(Debug)]
struct CustomPoint {x: u32, y: u32}
impl Default for CustomPoint {
fn default() -> Self {
CustomPoint {x: 42, y: 42}
}
}
fn main() {
let x = Point::default(); // Creates a Point{0, 0}
println!("{x:?}");
let y = CustomPoint::default();
println!("{y:?}");
}
Rust Default Trait
DefaultTrait 有多种用途,包括- 执行部分复制,其余用默认初始化
- 作为
Option类型在unwrap_or_default()等方法中的默认替代
#[derive(Debug)]
struct CustomPoint {x: u32, y: u32}
impl Default for CustomPoint {
fn default() -> Self {
CustomPoint {x: 42, y: 42}
}
}
fn main() {
let x = CustomPoint::default();
// Override y, but leave rest of elements as the default
let y = CustomPoint {y: 43, ..CustomPoint::default()};
println!("{x:?} {y:?}");
let z : Option<CustomPoint> = None;
// Try changing the unwrap_or_default() to unwrap()
println!("{:?}", z.unwrap_or_default());
}
其他 Rust 类型转换
- Rust 不支持隐式类型转换,
as可用于显式转换 as应谨慎使用,因为它可能因窄化等导致数据丢失。一般而言,尽可能使用into()或from()
fn main() {
let f = 42u8;
// let g : u32 = f; // Will not compile
let g = f as u32; // Ok, but not preferred. Subject to rules around narrowing
let g : u32 = f.into(); // Most preferred form; infallible and checked by the compiler
// let k : u8 = g.into(); // Fails to compile; narrowing can result in loss of data
// Attempting a narrowing operation requires use of try_into
if let Ok(k) = TryInto::<u8>::try_into(g) {
println!("{k}");
}
}
12. 闭包
Rust 闭包
你将学到: 闭包作为匿名函数、三种捕获 Trait(
Fn、FnMut、FnOnce)、move闭包,以及 Rust 闭包与 C++ lambda 的对比——自动捕获分析,而非手动的[&]/[=]指定。
- 闭包是可以捕获其环境的匿名函数
- C++ 等价物:lambda(
[&](int x) { return x + 1; }) - 关键差异:Rust 闭包有三种捕获 Trait(
Fn、FnMut、FnOnce),由编译器自动选择 - C++ 捕获模式(
[=]、[&]、[this])是手动的且容易出错(悬垂的[&]!) - Rust 的借用检查器在编译期防止悬垂捕获
- C++ 等价物:lambda(
- 闭包可通过
||符号识别。类型参数包含在||内,可使用类型推断 - 闭包常与迭代器配合使用(下一主题)
fn add_one(x: u32) -> u32 {
x + 1
}
fn main() {
let add_one_v1 = |x : u32| {x + 1}; // Explicitly specified type
let add_one_v2 = |x| {x + 1}; // Type is inferred from call site
let add_one_v3 = |x| x+1; // Permitted for single line functions
println!("{} {} {} {}", add_one(42), add_one_v1(42), add_one_v2(42), add_one_v3(42) );
}
练习:闭包与捕获
🟡 进阶
- 创建一个从外围作用域捕获
String并追加内容的闭包(提示:使用move) - 创建一个闭包向量:
Vec<Box<dyn Fn(i32) -> i32>>,包含加 1、乘 2 和平方输入的闭包。遍历该向量,对每个闭包应用数字 5
Solution (click to expand)
fn main() {
// Part 1: Closure that captures and appends to a String
let mut greeting = String::from("Hello");
let mut append = |suffix: &str| {
greeting.push_str(suffix);
};
append(", world");
append("!");
println!("{greeting}"); // "Hello, world!"
// Part 2: Vector of closures
let operations: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|x| x + 1), // add 1
Box::new(|x| x * 2), // multiply by 2
Box::new(|x| x * x), // square
];
let input = 5;
for (i, op) in operations.iter().enumerate() {
println!("Operation {i} on {input}: {}", op(input));
}
}
// Output:
// Hello, world!
// Operation 0 on 5: 6
// Operation 1 on 5: 10
// Operation 2 on 5: 25
Rust 迭代器
- 迭代器是 Rust 最强大的特性之一。它们为对集合执行操作提供了非常优雅的方法,包括过滤(
filter())、转换(map())、过滤并映射(filter_map())、搜索(find())等等 - 在以下示例中,
|&x| *x >= 42是执行相同比较的闭包。|x| println!("{x}")是另一个闭包
fn main() {
let a = [0, 1, 2, 3, 42, 43];
for x in &a {
if *x >= 42 {
println!("{x}");
}
}
// Same as above
a.iter().filter(|&x| *x >= 42).for_each(|x| println!("{x}"))
}
Rust 迭代器
- 迭代器的一个关键特性是大多数是
lazy的,即在被求值之前不会做任何事。例如,a.iter().filter(|&x| *x >= 42);没有for_each就不会做任何事。Rust 编译器在检测到这种情况时会发出显式警告
fn main() {
let a = [0, 1, 2, 3, 42, 43];
// Add one to each element and print it
let _ = a.iter().map(|x|x + 1).for_each(|x|println!("{x}"));
let found = a.iter().find(|&x|*x == 42);
println!("{found:?}");
// Count elements
let count = a.iter().count();
println!("{count}");
}
Rust 迭代器
collect()方法可用于将结果收集到单独的集合中- 在下方,
Vec<_>中的_相当于map返回类型的通配符。例如,我们甚至可以从map返回String
- 在下方,
fn main() {
let a = [0, 1, 2, 3, 42, 43];
let squared_a : Vec<_> = a.iter().map(|x|x*x).collect();
for x in &squared_a {
println!("{x}");
}
let squared_a_strings : Vec<_> = a.iter().map(|x|(x*x).to_string()).collect();
// These are actually string representations
for x in &squared_a_strings {
println!("{x}");
}
}
练习:Rust 迭代器
🟢 入门
- 创建一个由奇数和偶数元素组成的整数数组。遍历数组,将其拆分为两个向量,分别包含偶数和奇数元素
- 能否一次遍历完成(提示:使用
partition())?
Solution (click to expand)
fn main() {
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Approach 1: Manual iteration
let mut evens = Vec::new();
let mut odds = Vec::new();
for n in numbers {
if n % 2 == 0 {
evens.push(n);
} else {
odds.push(n);
}
}
println!("Evens: {evens:?}");
println!("Odds: {odds:?}");
// Approach 2: Single pass with partition()
let (evens, odds): (Vec<i32>, Vec<i32>) = numbers
.into_iter()
.partition(|n| *n % 2 == 0);
println!("Evens (partition): {evens:?}");
println!("Odds (partition): {odds:?}");
}
// Output:
// Evens: [2, 4, 6, 8, 10]
// Odds: [1, 3, 5, 7, 9]
// Evens (partition): [2, 4, 6, 8, 10]
// Odds (partition): [1, 3, 5, 7, 9]
生产模式:参见用闭包折叠赋值金字塔,了解生产 Rust 代码中的真实迭代器链(
.map().collect()、.filter().collect()、.find_map())。
迭代器强力工具:替代 C++ 循环的方法
以下迭代器适配器在生产 Rust 代码中广泛使用。C++ 有
<algorithm> 和 C++20 ranges,但 Rust 的迭代器链更可组合、
也更常用。
enumerate——索引 + 值(替代 for (int i = 0; ...))
#![allow(unused)]
fn main() {
let sensors = vec!["temp0", "temp1", "temp2"];
for (idx, name) in sensors.iter().enumerate() {
println!("Sensor {idx}: {name}");
}
// Sensor 0: temp0
// Sensor 1: temp1
// Sensor 2: temp2
}
C++ 等价物:for (size_t i = 0; i < sensors.size(); ++i) { auto& name = sensors[i]; ... }
zip——配对两个迭代器的元素(替代并行索引循环)
#![allow(unused)]
fn main() {
let names = ["gpu0", "gpu1", "gpu2"];
let temps = [72.5, 68.0, 75.3];
let report: Vec<String> = names.iter()
.zip(temps.iter())
.map(|(name, temp)| format!("{name}: {temp}°C"))
.collect();
println!("{report:?}");
// ["gpu0: 72.5°C", "gpu1: 68.0°C", "gpu2: 75.3°C"]
// Stops at the shorter iterator — no out-of-bounds risk
}
C++ 等价物:for (size_t i = 0; i < std::min(names.size(), temps.size()); ++i) { ... }
flat_map——map + 展平嵌套集合
#![allow(unused)]
fn main() {
// Each GPU has multiple PCIe BDFs; collect all BDFs across all GPUs
let gpu_bdfs = vec![
vec!["0000:01:00.0", "0000:02:00.0"],
vec!["0000:41:00.0"],
vec!["0000:81:00.0", "0000:82:00.0"],
];
let all_bdfs: Vec<&str> = gpu_bdfs.iter()
.flat_map(|bdfs| bdfs.iter().copied())
.collect();
println!("{all_bdfs:?}");
// ["0000:01:00.0", "0000:02:00.0", "0000:41:00.0", "0000:81:00.0", "0000:82:00.0"]
}
C++ 等价物:嵌套 for 循环推入单个向量。
chain——连接两个迭代器
#![allow(unused)]
fn main() {
let critical_gpus = vec!["gpu0", "gpu3"];
let warning_gpus = vec!["gpu1", "gpu5"];
// Process all flagged GPUs, critical first
for gpu in critical_gpus.iter().chain(warning_gpus.iter()) {
println!("Flagged: {gpu}");
}
}
windows 和 chunks——切片上的滑动/固定大小视图
#![allow(unused)]
fn main() {
let temps = [70, 72, 75, 73, 71, 68, 65];
// windows(3): sliding window of size 3 — detect trends
let rising = temps.windows(3)
.any(|w| w[0] < w[1] && w[1] < w[2]);
println!("Rising trend detected: {rising}"); // true (70 < 72 < 75)
// chunks(2): fixed-size groups — process in pairs
for pair in temps.chunks(2) {
println!("Pair: {pair:?}");
}
// Pair: [70, 72]
// Pair: [75, 73]
// Pair: [71, 68]
// Pair: [65] ← last chunk can be smaller
}
C++ 等价物:手动索引运算 i 和 i+1/i+2。
fold——累加为单个值(替代 std::accumulate)
#![allow(unused)]
fn main() {
let errors = vec![
("gpu0", 3u32),
("gpu1", 0),
("gpu2", 7),
("gpu3", 1),
];
// Count total errors and build summary in one pass
let (total, summary) = errors.iter().fold(
(0u32, String::new()),
|(count, mut s), (name, errs)| {
if *errs > 0 {
s.push_str(&format!("{name}:{errs} "));
}
(count + errs, s)
},
);
println!("Total errors: {total}, details: {summary}");
// Total errors: 11, details: gpu0:3 gpu2:7 gpu3:1
}
scan——有状态的转换(运行总和、增量检测)
#![allow(unused)]
fn main() {
let readings = [100, 105, 103, 110, 108];
// Compute deltas between consecutive readings
let deltas: Vec<i32> = readings.iter()
.scan(None::<i32>, |prev, &val| {
let delta = prev.map(|p| val - p);
*prev = Some(val);
Some(delta)
})
.flatten() // Remove the initial None
.collect();
println!("Deltas: {deltas:?}"); // [5, -2, 7, -2]
}
快速参考:C++ 循环 → Rust 迭代器
| C++ 模式 | Rust 迭代器 | 示例 |
|---|---|---|
for (int i = 0; i < v.size(); i++) | .enumerate() | v.iter().enumerate() |
| 带索引的并行迭代 | .zip() | a.iter().zip(b.iter()) |
| 嵌套循环 → 扁平结果 | .flat_map() | vecs.iter().flat_map(|v| v.iter()) |
| 连接两个容器 | .chain() | a.iter().chain(b.iter()) |
滑动窗口 v[i..i+n] | .windows(n) | v.windows(3) |
| 按固定大小分组处理 | .chunks(n) | v.chunks(4) |
std::accumulate / 手动累加器 | .fold() | .fold(init, |acc, x| ...) |
| 运行总和 / 增量跟踪 | .scan() | .scan(state, |s, x| ...) |
while (it != end && count < n) { ++it; ++count; } | .take(n) | .iter().take(5) |
while (it != end && !pred(*it)) { ++it; } | .skip_while() | .skip_while(|x| x < &threshold) |
std::any_of | .any() | .iter().any(|x| x > &limit) |
std::all_of | .all() | .iter().all(|x| x.is_valid()) |
std::none_of | !.any() | !iter.any(|x| x.failed()) |
std::count_if | .filter().count() | .filter(|x| x > &0).count() |
std::min_element / std::max_element | .min() / .max() | .iter().max() → Option<&T> |
std::unique | .dedup()(对已排序) | v.dedup()(对 Vec 原地操作) |
练习:迭代器链
给定传感器数据 Vec<(String, f64)>(名称、温度),编写一条
迭代器链:
- 过滤温度 > 80.0 的传感器
- 按温度降序排序
- 将每个格式化为
"{name}: {temp}°C [ALARM]" - 收集为
Vec<String>
提示:排序需要 Vec,因此 .sort_by() 之前需要 .collect()。
Solution (click to expand)
fn alarm_report(sensors: &[(String, f64)]) -> Vec<String> {
let mut hot: Vec<_> = sensors.iter()
.filter(|(_, temp)| *temp > 80.0)
.collect();
hot.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
hot.iter()
.map(|(name, temp)| format!("{name}: {temp}°C [ALARM]"))
.collect()
}
fn main() {
let sensors = vec![
("gpu0".to_string(), 72.5),
("gpu1".to_string(), 85.3),
("gpu2".to_string(), 91.0),
("gpu3".to_string(), 78.0),
("gpu4".to_string(), 88.7),
];
for line in alarm_report(&sensors) {
println!("{line}");
}
}
// Output:
// gpu2: 91°C [ALARM]
// gpu4: 88.7°C [ALARM]
// gpu1: 85.3°C [ALARM]
Rust 迭代器
IteratorTrait 用于为用户定义类型实现迭代(https://doc.rust-lang.org/std/iter/trait.IntoIterator.html)- 在示例中,我们将为斐波那契数列实现迭代器,它以 1, 1, 2, … 开始,后继数是前两个数之和
Iterator中的associated type(type Item = u32;)定义迭代器的输出类型(u32)next()方法包含实现迭代器的逻辑。此例中,所有状态信息都在Fibonacci结构体中- 我们也可以实现名为
IntoIterator的另一个 Trait,为更专门的迭代器实现into_iter()方法 - ▶ 在 Rust Playground 中尝试
迭代器利器
迭代器强力工具参考
你将学到: 超越
filter/map/collect的高级迭代器组合子——enumerate、zip、chain、flat_map、scan、windows和chunks。对于用安全、表达力强的 Rust 迭代器替代 C 风格带索引for循环至关重要。
基本的 filter/map/collect 链覆盖许多场景,但 Rust 的迭代器库
丰富得多。本节涵盖你日常会用到的工具——尤其是将手动跟踪索引、
累加结果或按固定大小块处理数据的 C 循环翻译为 Rust 时。
快速参考表
| 方法 | C 等价物 | 作用 | 返回 |
|---|---|---|---|
enumerate() | for (int i=0; ...) | 将每个元素与其索引配对 | (usize, T) |
zip(other) | 相同索引的并行数组 | 配对两个迭代器的元素 | (A, B) |
chain(other) | 先处理 array1 再处理 array2 | 连接两个迭代器 | T |
flat_map(f) | 嵌套循环 | map 后展平一层 | U |
windows(n) | for (int i=0; i<len-n+1; i++) &arr[i..i+n] | 大小为 n 的重叠切片 | &[T] |
chunks(n) | 每次处理 n 个元素 | 大小为 n 的非重叠切片 | &[T] |
fold(init, f) | int acc = init; for (...) acc = f(acc, x); | 归约为单个值 | Acc |
scan(init, f) | 带输出的运行累加器 | 类似 fold 但产生中间结果 | Option<B> |
take(n) / skip(n) | 从偏移开始循环 / 限制次数 | 前 n 个 / 跳过前 n 个元素 | T |
take_while(f) / skip_while(f) | while (pred) {...} | 谓词成立时取/跳过 | T |
peekable() | 用 arr[i+1] 前瞻 | 允许 .peek() 而不消耗 | T |
step_by(n) | for (i=0; i<len; i+=n) | 取每第 n 个元素 | T |
unzip() | 拆分并行数组 | 将配对收集为两个集合 | (A, B) |
sum() / product() | 累加和/积 | 用 + 或 * 归约 | T |
min() / max() | 找极值 | 返回 Option<T> | Option<T> |
any(f) / all(f) | bool found = false; for (...) ... | 短路布尔搜索 | bool |
position(f) | for (i=0; ...) if (pred) return i; | 首个匹配的索引 | Option<usize> |
enumerate——索引 + 值(替代 C 索引循环)
fn main() {
let sensors = ["GPU_TEMP", "CPU_TEMP", "FAN_RPM", "PSU_WATT"];
// C style: for (int i = 0; i < 4; i++) printf("[%d] %s\n", i, sensors[i]);
for (i, name) in sensors.iter().enumerate() {
println!("[{i}] {name}");
}
// Find the index of a specific sensor
let gpu_idx = sensors.iter().position(|&s| s == "GPU_TEMP");
println!("GPU sensor at index: {gpu_idx:?}"); // Some(0)
}
zip——并行迭代(替代并行数组循环)
fn main() {
let names = ["accel_diag", "nic_diag", "cpu_diag"];
let statuses = [true, false, true];
let durations_ms = [1200, 850, 3400];
// C: for (int i=0; i<3; i++) printf("%s: %s (%d ms)\n", names[i], ...);
for ((name, passed), ms) in names.iter().zip(&statuses).zip(&durations_ms) {
let status = if *passed { "PASS" } else { "FAIL" };
println!("{name}: {status} ({ms} ms)");
}
}
chain——连接迭代器
fn main() {
let critical = vec!["ECC error", "Thermal shutdown"];
let warnings = vec!["Link degraded", "Fan slow"];
// Process all events in priority order
let all_events: Vec<_> = critical.iter().chain(warnings.iter()).collect();
println!("{all_events:?}");
// ["ECC error", "Thermal shutdown", "Link degraded", "Fan slow"]
}
flat_map——展平嵌套结果
fn main() {
let lines = vec!["gpu:42:ok", "nic:99:fail", "cpu:7:ok"];
// Extract all numeric values from colon-separated lines
let numbers: Vec<u32> = lines.iter()
.flat_map(|line| line.split(':'))
.filter_map(|token| token.parse::<u32>().ok())
.collect();
println!("{numbers:?}"); // [42, 99, 7]
}
windows 和 chunks——滑动与固定大小分组
fn main() {
let temps = [65, 68, 72, 71, 75, 80, 78, 76];
// windows(3): overlapping groups of 3 (like a sliding average)
// C: for (int i = 0; i <= len-3; i++) avg(arr[i], arr[i+1], arr[i+2]);
let moving_avg: Vec<f64> = temps.windows(3)
.map(|w| w.iter().sum::<i32>() as f64 / 3.0)
.collect();
println!("Moving avg: {moving_avg:.1?}");
// chunks(2): non-overlapping groups of 2
// C: for (int i = 0; i < len; i += 2) process(arr[i], arr[i+1]);
for pair in temps.chunks(2) {
println!("Chunk: {pair:?}");
}
// chunks_exact(2): same but panics if remainder exists
// Also: .remainder() gives leftover elements
}
fold 和 scan——累加
fn main() {
let values = [10, 20, 30, 40, 50];
// fold: single final result (like C's accumulator loop)
let sum = values.iter().fold(0, |acc, &x| acc + x);
println!("Sum: {sum}"); // 150
// Build a string with fold
let csv = values.iter()
.fold(String::new(), |acc, x| {
if acc.is_empty() { format!("{x}") }
else { format!("{acc},{x}") }
});
println!("CSV: {csv}"); // "10,20,30,40,50"
// scan: like fold but yields intermediate results
let running_sum: Vec<i32> = values.iter()
.scan(0, |state, &x| {
*state += x;
Some(*state)
})
.collect();
println!("Running sum: {running_sum:?}"); // [10, 30, 60, 100, 150]
}
练习:传感器数据管道
给定原始传感器读数(每行一个,格式 "sensor_name:value:unit"),编写
迭代器管道:
- 将每行解析为
(name, f64, unit) - 过滤低于阈值的读数
- 使用
fold按传感器名称分组到HashMap - 打印每个传感器的平均读数
// Starter code
fn main() {
let raw_data = vec![
"gpu_temp:72.5:C",
"cpu_temp:65.0:C",
"gpu_temp:74.2:C",
"fan_rpm:1200.0:RPM",
"cpu_temp:63.8:C",
"gpu_temp:80.1:C",
"fan_rpm:1150.0:RPM",
];
let threshold = 70.0;
// TODO: Parse, filter values >= threshold, group by name, compute averages
}
Solution (click to expand)
use std::collections::HashMap;
fn main() {
let raw_data = vec![
"gpu_temp:72.5:C",
"cpu_temp:65.0:C",
"gpu_temp:74.2:C",
"fan_rpm:1200.0:RPM",
"cpu_temp:63.8:C",
"gpu_temp:80.1:C",
"fan_rpm:1150.0:RPM",
];
let threshold = 70.0;
// Parse → filter → group → average
let grouped = raw_data.iter()
.filter_map(|line| {
let parts: Vec<&str> = line.splitn(3, ':').collect();
if parts.len() == 3 {
let value: f64 = parts[1].parse().ok()?;
Some((parts[0], value, parts[2]))
} else {
None
}
})
.filter(|(_, value, _)| *value >= threshold)
.fold(HashMap::<&str, Vec<f64>>::new(), |mut acc, (name, value, _)| {
acc.entry(name).or_default().push(value);
acc
});
for (name, values) in &grouped {
let avg = values.iter().sum::<f64>() / values.len() as f64;
println!("{name}: avg={avg:.1} ({} readings)", values.len());
}
}
// Output (order may vary):
// gpu_temp: avg=75.6 (3 readings)
// fan_rpm: avg=1175.0 (2 readings)
Rust 迭代器
IteratorTrait 用于为用户定义类型实现迭代(https://doc.rust-lang.org/std/iter/trait.IntoIterator.html)- 在示例中,我们将为斐波那契数列实现迭代器,它以 1, 1, 2, … 开始,后继数是前两个数之和
Iterator中的associated type(type Item = u32;)定义迭代器的输出类型(u32)next()方法包含实现迭代器的逻辑。此例中,所有状态信息都在Fibonacci结构体中- 我们也可以实现名为
IntoIterator的另一个 Trait,为更专门的迭代器实现into_iter()方法 - https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ab367dc2611e1b5a0bf98f1185b38f3f
Rust 并发
你将学到: Rust 的并发模型——线程、
Send/Sync标记 Trait、Mutex<T>、Arc<T>、通道,以及编译器如何在编译期防止数据竞争。未使用的线程安全无运行时开销。
- Rust 内置并发支持,类似 C++ 中的
std::thread- 关键差异:Rust 通过
Send和Sync标记 Trait 在编译期防止数据竞争 - 在 C++ 中,在没有 mutex 的情况下跨线程共享
std::vector是 UB 但能编译通过。在 Rust 中,无法编译。 - Rust 中的
Mutex<T>包装的是数据,而不只是访问——不锁定就无法读取数据
- 关键差异:Rust 通过
thread::spawn()可用于创建单独线程,并行执行闭包||
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 0..10 {
println!("Count in thread: {i}!");
thread::sleep(Duration::from_millis(5));
}
});
for i in 0..5 {
println!("Main thread: {i}");
thread::sleep(Duration::from_millis(5));
}
handle.join().unwrap(); // The handle.join() ensures that the spawned thread exits
}
Rust 并发
- 当需要从环境借用时,可使用
thread::scope()。这可行是因为thread::scope会等待内部线程返回 - 尝试不使用
thread::scope执行此练习以了解问题
use std::thread;
fn main() {
let a = [0, 1, 2];
thread::scope(|scope| {
scope.spawn(|| {
for x in &a {
println!("{x}");
}
});
});
}
Rust 并发
- 也可使用
move将所有权转移给线程。对于像[i32; 3]这样的Copy类型,move关键字将数据复制到闭包中,原变量仍可使用
use std::thread;
fn main() {
let mut a = [0, 1, 2];
let handle = thread::spawn(move || {
for x in a {
println!("{x}");
}
});
a[0] = 42; // Doesn't affect the copy sent to the thread
handle.join().unwrap();
}
Rust 并发
Arc<T>可用于在多个线程间共享只读引用Arc表示 Atomic Reference Counted(原子引用计数)。引用直到引用计数为 0 才释放Arc::clone()仅增加引用计数,不克隆数据
use std::sync::Arc;
use std::thread;
fn main() {
let a = Arc::new([0, 1, 2]);
let mut handles = Vec::new();
for i in 0..2 {
let arc = Arc::clone(&a);
handles.push(thread::spawn(move || {
println!("Thread: {i} {arc:?}");
}));
}
handles.into_iter().for_each(|h| h.join().unwrap());
}
Rust 并发
Arc<T>可与Mutex<T>结合提供可变引用。Mutex保护受保护的数据,确保只有持有锁的线程才能访问。MutexGuard在超出作用域时自动释放(RAII)。注意:std::mem::forget仍可能泄漏 guard——因此「不可能忘记解锁」比「不可能泄漏」更准确。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..5 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
// MutexGuard dropped here — lock released automatically
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap());
// Output: Final count: 5
}
Rust 并发:RwLock
RwLock<T>允许多个并发读者或一个独占写者——C++ 中的读写锁模式(std::shared_mutex)- 读远多于写时使用
RwLock(例如配置、缓存) - 读写频率相近或临界区较短时使用
Mutex
- 读远多于写时使用
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let config = Arc::new(RwLock::new(String::from("v1.0")));
let mut handles = Vec::new();
// Spawn 5 readers — all can run concurrently
for i in 0..5 {
let config = Arc::clone(&config);
handles.push(thread::spawn(move || {
let val = config.read().unwrap(); // Multiple readers OK
println!("Reader {i}: {val}");
}));
}
// One writer — blocks until all readers finish
{
let config = Arc::clone(&config);
handles.push(thread::spawn(move || {
let mut val = config.write().unwrap(); // Exclusive access
*val = String::from("v2.0");
println!("Writer: updated to {val}");
}));
}
for handle in handles {
handle.join().unwrap();
}
}
Rust 并发:Mutex 中毒
- 如果线程在持有
Mutex或RwLock时 panic,锁会变为中毒状态- 后续对
.lock()的调用返回Err(PoisonError)——数据可能处于不一致状态 - 若确信数据仍有效,可用
.into_inner()恢复 - C++ 中没有等价物——
std::mutex没有中毒概念;panic 的线程只是让锁保持持有状态
- 后续对
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data2 = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut guard = data2.lock().unwrap();
guard.push(4);
panic!("oops!"); // Lock is now poisoned
});
let _ = handle.join(); // Thread panicked
// Subsequent lock attempts return Err(PoisonError)
match data.lock() {
Ok(guard) => println!("Data: {guard:?}"),
Err(poisoned) => {
println!("Lock was poisoned! Recovering...");
let guard = poisoned.into_inner(); // Access data anyway
println!("Recovered data: {guard:?}"); // [1, 2, 3, 4] — push succeeded before panic
}
}
}
Rust 并发:原子类型
- 对于简单计数器和标志,
std::sync::atomic类型可避免Mutex的开销AtomicBool、AtomicI32、AtomicU64、AtomicUsize等- 等价于 C++ 的
std::atomic<T>——相同的内存序模型(Relaxed、Acquire、Release、SeqCst)
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let counter = Arc::new(AtomicU64::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
counter.fetch_add(1, Ordering::Relaxed);
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Counter: {}", counter.load(Ordering::SeqCst));
// Output: Counter: 10000
}
| 原语 | 何时使用 | C++ 等价物 |
|---|---|---|
Mutex<T> | 一般可变共享状态 | std::mutex + 手动关联数据 |
RwLock<T> | 读多写少的工作负载 | std::shared_mutex |
Atomic* | 简单计数器、标志、无锁模式 | std::atomic<T> |
Condvar | 等待条件变为真 | std::condition_variable |
Rust 并发:Condvar
Condvar(条件变量)让线程休眠直到另一线程发出信号表明条件已改变- 始终与
Mutex配对——模式是:加锁、检查条件、未就绪则等待、就绪后行动 - 等价于 C++ 的
std::condition_variable/std::condition_variable::wait - 处理虚假唤醒——始终在循环中重新检查条件(或使用
wait_while/wait_until)
- 始终与
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
fn main() {
let pair = Arc::new((Mutex::new(false), Condvar::new()));
// Spawn a worker that waits for a signal
let pair2 = Arc::clone(&pair);
let worker = thread::spawn(move || {
let (lock, cvar) = &*pair2;
let mut ready = lock.lock().unwrap();
// wait: sleeps until signaled (always re-check in a loop for spurious wakeups)
while !*ready {
ready = cvar.wait(ready).unwrap();
}
println!("Worker: condition met, proceeding!");
});
// Main thread does some work, then signals the worker
thread::sleep(std::time::Duration::from_millis(100));
{
let (lock, cvar) = &*pair;
let mut ready = lock.lock().unwrap();
*ready = true;
cvar.notify_one(); // Wake one waiting thread (notify_all() wakes all)
}
worker.join().unwrap();
}
何时使用 Condvar 与通道: 当线程共享可变状态并需要等待该状态上的条件时使用
Condvar(例如「缓冲区非空」)。当线程需要传递消息时使用通道(mpsc)。通道通常更易推理。
Rust 并发
- Rust 通道可用于在
Sender和Receiver之间交换消息- 这使用称为
mpsc或Multi-producer, Single-Consumer的范式 send()和recv()都可能阻塞线程
- 这使用称为
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
tx.send(10).unwrap();
tx.send(20).unwrap();
println!("Received: {:?}", rx.recv());
println!("Received: {:?}", rx.recv());
let tx2 = tx.clone();
tx2.send(30).unwrap();
println!("Received: {:?}", rx.recv());
}
Rust 并发
- 通道可与线程结合
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
for _ in 0..2 {
let tx2 = tx.clone();
thread::spawn(move || {
let thread_id = thread::current().id();
for i in 0..10 {
tx2.send(format!("Message {i}")).unwrap();
println!("{thread_id:?}: sent Message {i}");
}
println!("{thread_id:?}: done");
});
}
// Drop the original sender so rx.iter() terminates when all cloned senders are dropped
drop(tx);
thread::sleep(Duration::from_millis(100));
for msg in rx.iter() {
println!("Main: got {msg}");
}
}
Rust 为何能防止数据竞争:Send 与 Sync
- Rust 使用两个标记 Trait 在编译期强制线程安全:
Send:若类型可安全转移到另一线程,则该类型是SendSync:若类型可通过&T在线程间安全共享,则该类型是Sync
- 大多数类型自动是
Send + Sync。值得注意的例外:Rc<T>既非 Send 也非 Sync(在线程中使用Arc<T>)Cell<T>和RefCell<T>不是 Sync(使用Mutex<T>或RwLock<T>)- 裸指针(
*const T、*mut T)既非 Send 也非 Sync
- 这就是为什么编译器阻止你跨线程使用
Rc<T>——它根本没有实现Send Arc<Mutex<T>>是Rc<RefCell<T>>的线程安全等价物
直觉 (Jon Gjengset):把值想象成玩具。
Send= 你可以把玩具送给另一个孩子(线程)——转移所有权是安全的。Sync= 你可以让别人同时玩你的玩具——共享引用是安全的。Rc<T>有脆弱(非原子)引用计数;转移或共享它会破坏计数,因此它既非Send也非Sync。
练习:多线程词频统计
🔴 挑战——结合线程、Arc、Mutex 和 HashMap
- 给定
Vec<String>文本行,为每行 spawn 一个线程统计该行词数 - 使用
Arc<Mutex<HashMap<String, usize>>>收集结果 - 打印所有行的总词数
- Bonus:尝试用通道(
mpsc)而非共享状态实现
Solution (click to expand)
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let lines = vec![
"the quick brown fox".to_string(),
"jumps over the lazy dog".to_string(),
"the fox is quick".to_string(),
];
let word_counts: Arc<Mutex<HashMap<String, usize>>> =
Arc::new(Mutex::new(HashMap::new()));
let mut handles = vec![];
for line in &lines {
let line = line.clone();
let counts = Arc::clone(&word_counts);
handles.push(thread::spawn(move || {
for word in line.split_whitespace() {
let mut map = counts.lock().unwrap();
*map.entry(word.to_lowercase()).or_insert(0) += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
let counts = word_counts.lock().unwrap();
let total: usize = counts.values().sum();
println!("Word frequencies: {counts:#?}");
println!("Total words: {total}");
}
// Output (order may vary):
// Word frequencies: {
// "the": 3,
// "quick": 2,
// "brown": 1,
// "fox": 2,
// "jumps": 1,
// "over": 1,
// "lazy": 1,
// "dog": 1,
// "is": 1,
// }
// Total words: 13
14. Unsafe Rust 与 FFI
Unsafe Rust
你将学到: 何时及如何使用
unsafe——裸指针解引用、用于从 Rust 调用 C 及反向调用的 FFI(Foreign Function Interface)、用于字符串互操作的CString/CStr,以及如何围绕 unsafe 代码编写 Safe Rust 包装。
unsafe解锁通常被 Rust 编译器禁止的功能- 解引用裸指针
- 访问可变静态变量
- https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
- 能力越大,责任越大
unsafe告诉编译器「我,程序员,承担维护编译器通常保证的不变式的责任」- 必须保证没有别名化的可变与不可变引用、没有悬垂指针、没有无效引用,……
unsafe的使用应限制在尽可能小的作用域- 所有使用
unsafe的代码都应有描述假设的「safety」注释
Unsafe Rust 示例
unsafe fn harmless() {}
fn main() {
// Safety: We are calling a harmless unsafe function
unsafe {
harmless();
}
let a = 42u32;
let p = &a as *const u32;
// Safety: p is a valid pointer to a variable that will remain in scope
unsafe {
println!("{}", *p);
}
// Safety: Not safe; for illustration purposes only
let dangerous_buffer = 0xb8000 as *mut u32;
unsafe {
println!("About to go kaboom!!!");
*dangerous_buffer = 0; // This will SEGV on most modern machines
}
}
简单 FFI 示例(C 消费的 Rust 库函数)
FFI 字符串:CString 与 CStr
FFI 表示 Foreign Function Interface(外部函数接口)——Rust 用于调用其他语言(如 C)编写的函数及反向调用的机制。
与 C 代码交互时,Rust 的 String 和 &str 类型(UTF-8 且无 null 终止符)与 C 字符串(null 终止的字节数组)不直接兼容。Rust 在 std::ffi 中提供 CString(拥有)和 CStr(借用):
| 类型 | 类比 | 何时使用 |
|---|---|---|
CString | String(拥有) | 从 Rust 数据创建 C 字符串 |
&CStr | &str(借用) | 从外部代码接收 C 字符串 |
#![allow(unused)]
fn main() {
use std::ffi::{CString, CStr};
use std::os::raw::c_char;
fn demo_ffi_strings() {
// Creating a C-compatible string (adds null terminator)
let c_string = CString::new("Hello from Rust").expect("CString::new failed");
let ptr: *const c_char = c_string.as_ptr();
// Converting a C string back to Rust (unsafe because we trust the pointer)
// Safety: ptr is valid and null-terminated (we just created it above)
let back_to_rust: &CStr = unsafe { CStr::from_ptr(ptr) };
let rust_str: &str = back_to_rust.to_str().expect("Invalid UTF-8");
println!("{}", rust_str);
}
}
警告:若输入包含内部 null 字节(
\0),CString::new()会返回错误。始终处理Result。下文 FFI 示例中会大量使用CStr。
FFI方法必须用#[no_mangle]标记,以确保编译器不修改(mangle)函数名- 我们将 crate 编译为静态库
#[no_mangle] pub extern "C" fn add(left: u64, right: u64) -> u64 { left + right } - 我们将编译以下 C 代码并链接到静态库。
#include <stdio.h> #include <stdint.h> extern uint64_t add(uint64_t, uint64_t); int main() { printf("Add returned %llu\n", add(21, 21)); }
复杂 FFI 示例
- 在以下示例中,我们将创建 Rust 日志接口并暴露给
[PYTHON] 和
C- 我们将看到同一接口如何原生用于 Rust 和 C
- 我们将探索使用
cbindgen等工具为C生成头文件 - 我们将看到
unsafe包装如何作为通往 Safe Rust 代码的桥梁
Logger 辅助函数
#![allow(unused)]
fn main() {
fn create_or_open_log_file(log_file: &str, overwrite: bool) -> Result<File, String> {
if overwrite {
File::create(log_file).map_err(|e| e.to_string())
} else {
OpenOptions::new()
.write(true)
.append(true)
.open(log_file)
.map_err(|e| e.to_string())
}
}
fn log_to_file(file_handle: &mut File, message: &str) -> Result<(), String> {
file_handle
.write_all(message.as_bytes())
.map_err(|e| e.to_string())
}
}
Logger 结构体
#![allow(unused)]
fn main() {
struct SimpleLogger {
log_level: LogLevel,
file_handle: File,
}
impl SimpleLogger {
fn new(log_file: &str, overwrite: bool, log_level: LogLevel) -> Result<Self, String> {
let file_handle = create_or_open_log_file(log_file, overwrite)?;
Ok(Self {
file_handle,
log_level,
})
}
fn log_message(&mut self, log_level: LogLevel, message: &str) -> Result<(), String> {
if log_level as u32 <= self.log_level as u32 {
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let message = format!("Simple: {timestamp} {log_level} {message}\n");
log_to_file(&mut self.file_handle, &message)
} else {
Ok(())
}
}
}
}
测试
- 用 Rust 测试功能很简单
- 测试方法用
#[test]装饰,不是编译二进制的一部分 - 很容易为测试目的创建 mock 方法
- 测试方法用
#![allow(unused)]
fn main() {
#[test]
fn testfunc() -> Result<(), String> {
let mut logger = SimpleLogger::new("test.log", false, LogLevel::INFO)?;
logger.log_message(LogLevel::TRACELEVEL1, "Hello world")?;
logger.log_message(LogLevel::CRITICAL, "Critical message")?;
Ok(()) // The compiler automatically drops logger here
}
}
cargo test
(C)-Rust FFI
- cbindgen 是生成导出 Rust 函数头文件的优秀工具
- 可用 cargo 安装
cargo install cbindgen
cbindgen
- 函数和结构体可使用
#[no_mangle]和#[repr(C)]导出- 我们假设常见的接口模式:传入指向实际实现的
**,成功返回 0,失败返回非零 - 不透明与透明结构体:我们的
SimpleLogger作为不透明指针(*mut SimpleLogger)传递——C 端从不访问其字段,因此不需要#[repr(C)]。当 C 代码需要直接读/写结构体字段时使用#[repr(C)]:
- 我们假设常见的接口模式:传入指向实际实现的
#![allow(unused)]
fn main() {
// Opaque — C only holds a pointer, never inspects fields. No #[repr(C)] needed.
struct SimpleLogger { /* Rust-only fields */ }
// Transparent — C reads/writes fields directly. MUST use #[repr(C)].
#[repr(C)]
pub struct Point {
pub x: f64,
pub y: f64,
}
}
typedef struct SimpleLogger SimpleLogger;
uint32_t create_simple_logger(const char *file_name, struct SimpleLogger **out_logger);
uint32_t log_entry(struct SimpleLogger *logger, const char *message);
uint32_t drop_logger(struct SimpleLogger *logger);
- 注意我们需要大量健全性检查
- 必须显式泄漏内存以防止 Rust 自动释放
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn create_simple_logger(file_name: *const std::os::raw::c_char, out_logger: *mut *mut SimpleLogger) -> u32 {
use std::ffi::CStr;
// Make sure pointer isn't NULL
if file_name.is_null() || out_logger.is_null() {
return 1;
}
// Safety: The passed in pointer is either NULL or 0-terminated by contract
let file_name = unsafe {
CStr::from_ptr(file_name)
};
let file_name = file_name.to_str();
// Make sure that file_name doesn't have garbage characters
if file_name.is_err() {
return 1;
}
let file_name = file_name.unwrap();
// Assume some defaults; we'll pass them in real life
let new_logger = SimpleLogger::new(file_name, true, LogLevel::CRITICAL);
// Check that we were able to construct the logger
if new_logger.is_err() {
return 1;
}
let new_logger = Box::new(new_logger.unwrap());
// This prevents the Box from being dropped when if goes out of scope
let logger_ptr: *mut SimpleLogger = Box::leak(new_logger);
// Safety: logger is non-null and logger_ptr is valid
unsafe {
*out_logger = logger_ptr;
}
return 0;
}
}
log_entry()中有类似的错误检查
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn log_entry(logger: *mut SimpleLogger, message: *const std::os::raw::c_char) -> u32 {
use std::ffi::CStr;
if message.is_null() || logger.is_null() {
return 1;
}
// Safety: message is non-null
let message = unsafe {
CStr::from_ptr(message)
};
let message = message.to_str();
// Make sure that file_name doesn't have garbage characters
if message.is_err() {
return 1;
}
// Safety: logger is valid pointer previously constructed by create_simple_logger()
unsafe {
(*logger).log_message(LogLevel::CRITICAL, message.unwrap()).is_err() as u32
}
}
#[no_mangle]
pub extern "C" fn drop_logger(logger: *mut SimpleLogger) -> u32 {
if logger.is_null() {
return 1;
}
// Safety: logger is valid pointer previously constructed by create_simple_logger()
unsafe {
// This constructs a Box<SimpleLogger>, which is dropped when it goes out of scope
let _ = Box::from_raw(logger);
}
0
}
}
- 可用 Rust 或编写 (C) 程序测试我们的 (C)-FFI
#![allow(unused)]
fn main() {
#[test]
fn test_c_logger() {
// The c".." creates a NULL terminated string
let file_name = c"test.log".as_ptr() as *const std::os::raw::c_char;
let mut c_logger: *mut SimpleLogger = std::ptr::null_mut();
assert_eq!(create_simple_logger(file_name, &mut c_logger), 0);
// This is the manual way to create c"..." strings
let message = b"message from C\0".as_ptr() as *const std::os::raw::c_char;
assert_eq!(log_entry(c_logger, message), 0);
drop_logger(c_logger);
}
}
#include "logger.h"
...
int main() {
SimpleLogger *logger = NULL;
if (create_simple_logger("test.log", &logger) == 0) {
log_entry(logger, "Hello from C");
drop_logger(logger); /*Needed to close handle, etc.*/
}
...
}
确保 unsafe 代码的正确性
- 简而言之,使用
unsafe需要深思熟虑- 始终记录代码的安全假设并与专家审查
- 使用 cbindgen、Miri、Valgrind 等工具帮助验证正确性
- 绝不要让 panic 跨越 FFI 边界展开——这是 UB。在 FFI 入口点使用
std::panic::catch_unwind,或在 profile 中配置panic = "abort" - 若结构体跨 FFI 共享,标记
#[repr(C)]以保证 C 兼容内存布局 - 查阅 https://doc.rust-lang.org/nomicon/intro.html(「Rustonomicon」—— unsafe Rust 的黑暗艺术)
- 寻求内部专家帮助
验证工具:Miri 与 Valgrind
C++ 开发者熟悉 Valgrind 和 sanitizer。Rust 拥有这些以及 Miri,对 Rust 特有的 UB 更精确:
| Miri | Valgrind | C++ sanitizer(ASan/MSan/UBSan) | |
|---|---|---|---|
| 能捕获什么 | Rust 特有 UB:stacked borrows、无效 enum 判别式、未初始化读取、别名违规 | 内存泄漏、释放后使用、无效读/写、未初始化内存 | 缓冲区溢出、释放后使用、数据竞争、UB |
| 工作原理 | 解释 MIR(Rust 中级 IR)——无原生执行 | 在运行时检测编译后的二进制 | 编译期插桩 |
| FFI 支持 | ❌ 无法跨越 FFI 边界(跳过 C 调用) | ✅ 适用于任何编译后的二进制,包括 FFI | ✅ 若 C 代码也用 sanitizer 编译则可用 |
| 速度 | 比原生慢约 100 倍 | 慢约 10–50 倍 | 慢约 2–5 倍 |
| 何时使用 | 纯 Rust unsafe 代码、数据结构不变式 | FFI 代码、完整二进制集成测试 | FFI 的 C/C++ 侧、性能敏感测试 |
| 捕获别名 bug | ✅ Stacked Borrows 模型 | ❌ | 部分(TSan 用于数据竞争) |
建议:两者都用——纯 Rust unsafe 用 Miri,FFI 集成用 Valgrind:
-
Miri——捕获 Valgrind 看不到的 Rust 特有 UB(别名违规、无效枚举值、stacked borrows):
rustup +nightly component add miri cargo +nightly miri test # Run all tests under Miri cargo +nightly miri test -- test_name # Run a specific test⚠️ Miri 需要 nightly,无法执行 FFI 调用。将 unsafe Rust 逻辑隔离为可测试单元。
-
Valgrind——你已熟悉的工具,适用于包括 FFI 在内的编译后二进制:
sudo apt install valgrind cargo install cargo-valgrind cargo valgrind test # Run all tests under Valgrind捕获 FFI 代码中常见的
Box::leak/Box::from_raw模式的泄漏。 -
cargo-careful——在启用额外运行时检查的情况下运行测试(介于常规测试与 Miri 之间):
cargo install cargo-careful cargo +nightly careful test
Unsafe Rust 总结
cbindgen是 (C) FFI 到 Rust 的优秀工具- 反向 FFI 接口使用
bindgen(查阅详尽文档)
- 反向 FFI 接口使用
- 不要假设你的 unsafe 代码是正确的,或从 Safe Rust 使用它没问题。很容易犯错,即使看似正确的代码也可能因微妙原因而出错
- 使用工具验证正确性
- 若仍有疑问,寻求专家意见
- 确保
unsafe代码有明确记录假设及为何正确的注释unsafe代码的调用者也应有相应的 safety 注释,并遵守限制
练习:编写 Safe Rust FFI 包装
🔴 挑战——需要理解 unsafe 块、裸指针和 Safe Rust API 设计
- 围绕
unsafeFFI 风格函数编写 Safe Rust 包装。练习模拟调用将格式化字符串写入调用者提供缓冲区的 C 函数。 - 步骤 1:实现 unsafe 函数
unsafe_greet,将问候语写入裸*mut u8缓冲区 - 步骤 2:编写 safe 包装
safe_greet,分配Vec<u8>,调用 unsafe 函数,返回String - 步骤 3:为每个 unsafe 块添加适当的
// Safety:注释
Starter code:
use std::fmt::Write as _;
/// Simulates a C function: writes "Hello, <name>!" into buffer.
/// Returns the number of bytes written (excluding null terminator).
/// # Safety
/// - `buf` must point to at least `buf_len` writable bytes
/// - `name` must be a valid pointer to a null-terminated C string
unsafe fn unsafe_greet(buf: *mut u8, buf_len: usize, name: *const u8) -> isize {
// TODO: Build greeting, copy bytes into buf, return length
// Hint: use std::ffi::CStr::from_ptr or iterate bytes manually
todo!()
}
/// Safe wrapper — no unsafe in the public API
fn safe_greet(name: &str) -> Result<String, String> {
// TODO: Allocate a Vec<u8> buffer, create a null-terminated name,
// call unsafe_greet inside an unsafe block with Safety comment,
// convert the result back to a String
todo!()
}
fn main() {
match safe_greet("Rustacean") {
Ok(msg) => println!("{msg}"),
Err(e) => eprintln!("Error: {e}"),
}
// Expected output: Hello, Rustacean!
}
Solution (click to expand)
use std::ffi::CStr;
/// Simulates a C function: writes "Hello, <name>!" into buffer.
/// Returns the number of bytes written, or -1 if buffer too small.
/// # Safety
/// - `buf` must point to at least `buf_len` writable bytes
/// - `name` must be a valid pointer to a null-terminated C string
unsafe fn unsafe_greet(buf: *mut u8, buf_len: usize, name: *const u8) -> isize {
// Safety: caller guarantees name is a valid null-terminated string
let name_cstr = unsafe { CStr::from_ptr(name as *const std::os::raw::c_char) };
let name_str = match name_cstr.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
let greeting = format!("Hello, {}!", name_str);
if greeting.len() > buf_len {
return -1;
}
// Safety: buf points to at least buf_len writable bytes (caller guarantee)
unsafe {
std::ptr::copy_nonoverlapping(greeting.as_ptr(), buf, greeting.len());
}
greeting.len() as isize
}
/// Safe wrapper — no unsafe in the public API
fn safe_greet(name: &str) -> Result<String, String> {
let mut buffer = vec![0u8; 256];
// Create a null-terminated version of name for the C API
let name_with_null: Vec<u8> = name.bytes().chain(std::iter::once(0)).collect();
// Safety: buffer has 256 writable bytes, name_with_null is null-terminated
let bytes_written = unsafe {
unsafe_greet(buffer.as_mut_ptr(), buffer.len(), name_with_null.as_ptr())
};
if bytes_written < 0 {
return Err("Buffer too small or invalid name".to_string());
}
String::from_utf8(buffer[..bytes_written as usize].to_vec())
.map_err(|e| format!("Invalid UTF-8: {e}"))
}
fn main() {
match safe_greet("Rustacean") {
Ok(msg) => println!("{msg}"),
Err(e) => eprintln!("Error: {e}"),
}
}
// Output:
// Hello, Rustacean!
no_std — 无标准库的 Rust
你将学到: 如何使用
#![no_std]为裸机和嵌入式目标编写 Rust——core与alloccrate 的分层、panic handler,以及与无libc的嵌入式 C 的对比。
如果你来自嵌入式 C,你已经习惯在没有 libc 或仅有极简运行时的情况下工作。Rust 有对应的一等公民方案:#![no_std] 属性。
什么是 no_std?
在 crate 根添加 #![no_std] 后,编译器会移除隐式的 extern crate std;,仅链接 core(以及可选的 alloc)。
| 层级 | 提供什么 | 需要 OS / 堆? |
|---|---|---|
core | 基本类型、Option、Result、Iterator、数学、slice、str、原子操作、fmt | 否 — 可在裸机上运行 |
alloc | Vec、String、Box、Rc、Arc、BTreeMap | 需要全局分配器,但不需要 OS |
std | HashMap、fs、net、thread、io、env、process | 是 — 需要 OS |
嵌入式开发者经验法则: 若你的 C 项目链接
-lc并使用malloc,通常可以用core+alloc。若在裸机上运行且没有malloc,只用core。
声明 no_std
#![allow(unused)]
fn main() {
// src/lib.rs (or src/main.rs for a binary with #![no_main])
#![no_std]
// You still get everything in `core`:
use core::fmt;
use core::result::Result;
use core::option::Option;
// If you have an allocator, opt in to heap types:
extern crate alloc;
use alloc::vec::Vec;
use alloc::string::String;
}
对于裸机二进制,还需要 #![no_main] 和 panic handler:
#![allow(unused)]
#![no_std]
#![no_main]
fn main() {
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {} // hang on panic — replace with your board's reset/LED blink
}
// Entry point depends on your HAL / linker script
}
你会失去什么(以及替代方案)
std 功能 | no_std 替代 |
|---|---|
println! | 向 UART 使用 core::write! / defmt |
HashMap | heapless::FnvIndexMap(固定容量)或 BTreeMap(配合 alloc) |
Vec | heapless::Vec(栈分配、固定容量) |
String | heapless::String 或 &str |
std::io::Read/Write | embedded_io::Read/Write |
thread::spawn | 中断处理程序、RTIC 任务 |
std::time | 硬件定时器外设 |
std::fs | Flash / EEPROM 驱动 |
嵌入式常用 no_std crate
| Crate | 用途 | 说明 |
|---|---|---|
heapless | 固定容量 Vec、String、Queue、Map | 无需分配器 — 全部在栈上 |
defmt | 经 probe/ITM 的高效日志 | 类似 printf,格式化延迟到主机端 |
embedded-hal | 硬件抽象 Trait(SPI、I²C、GPIO、UART) | 实现一次,可在任意 MCU 上运行 |
cortex-m | ARM Cortex-M intrinsic 与寄存器访问 | 底层,类似 CMSIS |
cortex-m-rt | Cortex-M 运行时 / 启动代码 | 替代你的 startup.s |
rtic | 实时中断驱动并发(Real-Time Interrupt-driven Concurrency) | 编译期任务调度,零开销 |
embassy | 嵌入式异步执行器 | 裸机上的 async/await |
postcard | no_std serde 序列化(二进制) | 无法用字符串时替代 serde_json |
thiserror | 为 Error Trait 派生宏 | v2 起支持 no_std;优先于 anyhow |
smoltcp | no_std TCP/IP 协议栈 | 无 OS 时需要网络时 |
C 与 Rust:裸机对比
典型的嵌入式 C blinky:
// C — bare metal, vendor HAL
#include "stm32f4xx_hal.h"
void SysTick_Handler(void) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
int main(void) {
HAL_Init();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio = { .Pin = GPIO_PIN_5, .Mode = GPIO_MODE_OUTPUT_PP };
HAL_GPIO_Init(GPIOA, &gpio);
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / 1000);
while (1) {}
}
Rust 等价实现(使用 embedded-hal + 板级 crate):
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _; // panic handler: infinite loop
use stm32f4xx_hal::{pac, prelude::*};
#[entry]
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let gpioa = dp.GPIOA.split();
let mut led = gpioa.pa5.into_push_pull_output();
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let mut delay = dp.TIM2.delay_ms(&clocks);
loop {
led.toggle();
delay.delay_ms(500u32);
}
}
C 开发者应关注的关键差异:
Peripherals::take()返回Option— 在编译期保证单例模式(无双初始化 bug).split()将各引脚的所有权移出 — 不会出现两个模块驱动同一引脚- 所有寄存器访问都有类型检查 — 不会误写只读寄存器
- 借用检查器防止
main与中断处理程序之间的数据竞争(配合 RTIC)
何时使用 no_std 与 std
flowchart TD
A[Does your target have an OS?] -->|Yes| B[Use std]
A -->|No| C[Do you have a heap allocator?]
C -->|Yes| D["Use #![no_std] + extern crate alloc"]
C -->|No| E["Use #![no_std] with core only"]
B --> F[Full Vec, HashMap, threads, fs, net]
D --> G[Vec, String, Box, BTreeMap — no fs/net/threads]
E --> H[Fixed-size arrays, heapless collections, no allocation]
练习:no_std 环形缓冲区
🔴 挑战 — 在 no_std 场景中结合泛型、MaybeUninit 与 #[cfg(test)]
在嵌入式系统中,你经常需要固定大小的环形缓冲区(循环缓冲区),且从不分配内存。仅用 core(不用 alloc、不用 std)实现一个。
要求:
- 对元素类型
T: Copy泛型化 - 固定容量
N(const 泛型) push(&mut self, item: T)— 满时覆盖最旧元素pop(&mut self) -> Option<T>— 返回最旧元素len(&self) -> usizeis_empty(&self) -> bool- 必须能在
#![no_std]下编译
#![allow(unused)]
fn main() {
// Starter code
#![no_std]
use core::mem::MaybeUninit;
pub struct RingBuffer<T: Copy, const N: usize> {
buf: [MaybeUninit<T>; N],
head: usize, // next write position
tail: usize, // next read position
count: usize,
}
impl<T: Copy, const N: usize> RingBuffer<T, N> {
pub const fn new() -> Self {
todo!()
}
pub fn push(&mut self, item: T) {
todo!()
}
pub fn pop(&mut self) -> Option<T> {
todo!()
}
pub fn len(&self) -> usize {
todo!()
}
pub fn is_empty(&self) -> bool {
todo!()
}
}
}
解答
#![allow(unused)]
#![no_std]
fn main() {
use core::mem::MaybeUninit;
pub struct RingBuffer<T: Copy, const N: usize> {
buf: [MaybeUninit<T>; N],
head: usize,
tail: usize,
count: usize,
}
impl<T: Copy, const N: usize> RingBuffer<T, N> {
pub const fn new() -> Self {
Self {
// SAFETY: MaybeUninit does not require initialization
buf: unsafe { MaybeUninit::uninit().assume_init() },
head: 0,
tail: 0,
count: 0,
}
}
pub fn push(&mut self, item: T) {
self.buf[self.head] = MaybeUninit::new(item);
self.head = (self.head + 1) % N;
if self.count == N {
// Buffer is full — overwrite oldest, advance tail
self.tail = (self.tail + 1) % N;
} else {
self.count += 1;
}
}
pub fn pop(&mut self) -> Option<T> {
if self.count == 0 {
return None;
}
// SAFETY: We only read positions that were previously written via push()
let item = unsafe { self.buf[self.tail].assume_init() };
self.tail = (self.tail + 1) % N;
self.count -= 1;
Some(item)
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_push_pop() {
let mut rb = RingBuffer::<u32, 4>::new();
assert!(rb.is_empty());
rb.push(10);
rb.push(20);
rb.push(30);
assert_eq!(rb.len(), 3);
assert_eq!(rb.pop(), Some(10));
assert_eq!(rb.pop(), Some(20));
assert_eq!(rb.pop(), Some(30));
assert_eq!(rb.pop(), None);
}
#[test]
fn overwrite_on_full() {
let mut rb = RingBuffer::<u8, 3>::new();
rb.push(1);
rb.push(2);
rb.push(3);
// Buffer full: [1, 2, 3]
rb.push(4); // Overwrites 1 → [4, 2, 3], tail advances
assert_eq!(rb.len(), 3);
assert_eq!(rb.pop(), Some(2)); // oldest surviving
assert_eq!(rb.pop(), Some(3));
assert_eq!(rb.pop(), Some(4));
assert_eq!(rb.pop(), None);
}
}
}
对嵌入式 C 开发者的意义:
MaybeUninit是 Rust 中未初始化内存的等价物 — 编译器不会插入零填充,就像 C 里的char buf[N];unsafe代码块很少(2 行),且每处都有// SAFETY:注释const fn new()意味着可在static变量中创建环形缓冲区,无需运行时构造函数- 测试可在主机上用
cargo test运行,尽管代码是no_std
嵌入式深入
MMIO 与 volatile 寄存器访问
你将学到: 嵌入式 Rust 中类型安全的硬件寄存器访问 — volatile MMIO 模式、寄存器抽象 crate,以及 Rust 类型系统如何表达 C 的
volatile关键字无法编码的寄存器权限。
在 C 固件中,你通过指向特定内存地址的 volatile 指针访问硬件寄存器。Rust 有等价机制 — 但具备类型安全。
C volatile 与 Rust volatile
// C — typical MMIO register access
#define GPIO_BASE 0x40020000
#define GPIO_MODER (*(volatile uint32_t*)(GPIO_BASE + 0x00))
#define GPIO_ODR (*(volatile uint32_t*)(GPIO_BASE + 0x14))
void toggle_led(void) {
GPIO_ODR ^= (1 << 5); // Toggle pin 5
}
#![allow(unused)]
fn main() {
// Rust — raw volatile (low-level, rarely used directly)
use core::ptr;
const GPIO_BASE: usize = 0x4002_0000;
const GPIO_ODR: *mut u32 = (GPIO_BASE + 0x14) as *mut u32;
/// # Safety
/// Caller must ensure GPIO_BASE is a valid mapped peripheral address.
unsafe fn toggle_led() {
// SAFETY: GPIO_ODR is a valid memory-mapped register address.
let current = unsafe { ptr::read_volatile(GPIO_ODR) };
unsafe { ptr::write_volatile(GPIO_ODR, current ^ (1 << 5)) };
}
}
svd2rust — 类型安全的寄存器访问(Rust 方式)
实践中,你从不手写 raw volatile 指针。相反,svd2rust 从芯片的 SVD 文件(与 IDE 调试视图使用的同一 XML 文件)生成 Peripheral Access Crate(PAC):
#![allow(unused)]
fn main() {
// Generated PAC code (you don't write this — svd2rust does)
// The PAC makes invalid register access a compile error
// Usage with PAC:
use stm32f4::stm32f401; // PAC crate for your chip
fn configure_gpio(dp: stm32f401::Peripherals) {
// Enable GPIOA clock — type-safe, no magic numbers
dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());
// Set pin 5 to output — can't accidentally write to a read-only field
dp.GPIOA.moder.modify(|_, w| w.moder5().output());
// Toggle pin 5 — type-checked field access
dp.GPIOA.odr.modify(|r, w| {
// SAFETY: toggling a single bit in a valid register field.
unsafe { w.bits(r.bits() ^ (1 << 5)) }
});
}
}
| C 寄存器访问 | Rust PAC 等价 |
|---|---|
#define REG (*(volatile uint32_t*)ADDR) | svd2rust 生成的 PAC crate |
| `REG | = BITMASK;` |
value = REG; | let val = periph.reg.read().field().bits() |
| 错误寄存器字段 → 静默 UB | 编译错误 — 字段不存在 |
| 错误寄存器位宽 → 静默 UB | 类型检查 — u8 vs u16 vs u32 |
中断处理与临界区
C 固件使用 __disable_irq() / __enable_irq() 以及 void 签名的 ISR 函数。Rust 提供类型安全的等价物。
C 与 Rust 中断模式
// C — traditional interrupt handler
volatile uint32_t tick_count = 0;
void SysTick_Handler(void) { // Naming convention is critical — get it wrong → HardFault
tick_count++;
}
uint32_t get_ticks(void) {
__disable_irq();
uint32_t t = tick_count; // Read inside critical section
__enable_irq();
return t;
}
#![allow(unused)]
fn main() {
// Rust — using cortex-m and critical sections
use core::cell::Cell;
use cortex_m::interrupt::{self, Mutex};
// Shared state protected by a critical-section Mutex
static TICK_COUNT: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
#[cortex_m_rt::exception] // Attribute ensures correct vector table placement
fn SysTick() { // Compile error if name doesn't match a valid exception
interrupt::free(|cs| { // cs = critical section token (proof IRQs disabled)
let count = TICK_COUNT.borrow(cs).get();
TICK_COUNT.borrow(cs).set(count + 1);
});
}
fn get_ticks() -> u32 {
interrupt::free(|cs| TICK_COUNT.borrow(cs).get())
}
}
RTIC — 实时中断驱动并发
对于具有多个中断优先级的复杂固件,RTIC(前身为 RTFM)提供编译期任务调度、零开销:
#![allow(unused)]
fn main() {
#[rtic::app(device = stm32f4xx_hal::pac, dispatchers = [USART1])]
mod app {
use stm32f4xx_hal::prelude::*;
#[shared]
struct Shared {
temperature: f32, // Shared between tasks — RTIC manages locking
}
#[local]
struct Local {
led: stm32f4xx_hal::gpio::Pin<'A', 5, stm32f4xx_hal::gpio::Output>,
}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
let dp = cx.device;
let gpioa = dp.GPIOA.split();
let led = gpioa.pa5.into_push_pull_output();
(Shared { temperature: 25.0 }, Local { led })
}
// Hardware task: runs on SysTick interrupt
#[task(binds = SysTick, shared = [temperature], local = [led])]
fn tick(mut cx: tick::Context) {
cx.local.led.toggle();
cx.shared.temperature.lock(|temp| {
// RTIC guarantees exclusive access here — no manual locking needed
*temp += 0.1;
});
}
}
}
RTIC 对 C 固件开发者的意义:
#[shared]注解替代手动互斥锁管理- 基于优先级的抢占在编译期配置 — 无运行时开销
- 结构上无死锁(框架在编译期证明)
- ISR 命名错误是编译错误,而非运行时 HardFault
Panic Handler 策略
在 C 中,固件出错时通常复位或闪烁 LED。Rust 的 panic handler 提供结构化控制:
#![allow(unused)]
fn main() {
// Strategy 1: Halt (for debugging — attach debugger, inspect state)
use panic_halt as _; // Infinite loop on panic
// Strategy 2: Reset the MCU
use panic_reset as _; // Triggers system reset
// Strategy 3: Log via probe (development)
use panic_probe as _; // Sends panic info over debug probe (with defmt)
// Strategy 4: Log over defmt then halt
use defmt_panic as _; // Rich panic messages over ITM/RTT
// Strategy 5: Custom handler (production firmware)
use core::panic::PanicInfo;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
// 1. Disable interrupts to prevent further damage
cortex_m::interrupt::disable();
// 2. Write panic info to a reserved RAM region (survives reset)
// SAFETY: PANIC_LOG is a reserved memory region defined in linker script.
unsafe {
let log = 0x2000_0000 as *mut [u8; 256];
// Write truncated panic message
use core::fmt::Write;
let mut writer = FixedWriter::new(&mut *log);
let _ = write!(writer, "{}", info);
}
// 3. Trigger watchdog reset (or blink error LED)
loop {
cortex_m::asm::wfi(); // Wait for interrupt (low power while halted)
}
}
}
链接脚本与内存布局
C 固件开发者编写链接脚本定义 FLASH/RAM 区域。Rust 嵌入式通过 memory.x 使用相同概念:
/* memory.x — placed at crate root, consumed by cortex-m-rt */
MEMORY
{
/* Adjust for your MCU — these are STM32F401 values */
FLASH : ORIGIN = 0x08000000, LENGTH = 512K
RAM : ORIGIN = 0x20000000, LENGTH = 96K
}
/* Optional: reserve space for panic log (see panic handler above) */
_panic_log_start = ORIGIN(RAM);
_panic_log_size = 256;
# .cargo/config.toml — set the target and linker flags
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32F401RE" # flash and run via debug probe
rustflags = [
"-C", "link-arg=-Tlink.x", # cortex-m-rt linker script
]
[build]
target = "thumbv7em-none-eabihf" # Cortex-M4F with hardware FPU
| C 链接脚本 | Rust 等价 |
|---|---|
MEMORY { FLASH ..., RAM ... } | crate 根目录的 memory.x |
__attribute__((section(".data"))) | #[link_section = ".data"] |
Makefile 中的 -T linker.ld | .cargo/config.toml 中的 -C link-arg=-Tlink.x |
__bss_start__、__bss_end__ | 由 cortex-m-rt 自动处理 |
启动汇编(startup.s) | cortex-m-rt 的 #[entry] 宏 |
编写 embedded-hal 驱动
embedded-hal crate 为 SPI、I2C、GPIO、UART 等定义 Trait。针对这些 Trait 编写的驱动可在任意 MCU 上工作 — 这是 Rust 在嵌入式复用上的杀手级特性。
C 与 Rust:温度传感器驱动
// C — driver tightly coupled to STM32 HAL
#include "stm32f4xx_hal.h"
float read_temperature(I2C_HandleTypeDef* hi2c, uint8_t addr) {
uint8_t buf[2];
HAL_I2C_Mem_Read(hi2c, addr << 1, 0x00, I2C_MEMADD_SIZE_8BIT,
buf, 2, HAL_MAX_DELAY);
int16_t raw = ((int16_t)buf[0] << 4) | (buf[1] >> 4);
return raw * 0.0625;
}
// Problem: This driver ONLY works with STM32 HAL. Porting to Nordic = rewrite.
#![allow(unused)]
fn main() {
// Rust — driver works on ANY MCU that implements embedded-hal
use embedded_hal::i2c::I2c;
pub struct Tmp102<I2C> {
i2c: I2C,
address: u8,
}
impl<I2C: I2c> Tmp102<I2C> {
pub fn new(i2c: I2C, address: u8) -> Self {
Self { i2c, address }
}
pub fn read_temperature(&mut self) -> Result<f32, I2C::Error> {
let mut buf = [0u8; 2];
self.i2c.write_read(self.address, &[0x00], &mut buf)?;
let raw = ((buf[0] as i16) << 4) | ((buf[1] as i16) >> 4);
Ok(raw as f32 * 0.0625)
}
}
// Works on STM32, Nordic nRF, ESP32, RP2040 — any chip with an embedded-hal I2C impl
}
graph TD
subgraph "C Driver Architecture"
CD["Temperature Driver"]
CD --> STM["STM32 HAL"]
CD -.->|"Port = REWRITE"| NRF["Nordic HAL"]
CD -.->|"Port = REWRITE"| ESP["ESP-IDF"]
end
subgraph "Rust embedded-hal Architecture"
RD["Temperature Driver<br/>impl<I2C: I2c>"]
RD --> EHAL["embedded-hal::I2c trait"]
EHAL --> STM2["stm32f4xx-hal"]
EHAL --> NRF2["nrf52-hal"]
EHAL --> ESP2["esp-hal"]
EHAL --> RP2["rp2040-hal"]
NOTE["Write driver ONCE,<br/>runs on ALL chips"]
end
style CD fill:#ffa07a,color:#000
style RD fill:#91e5a3,color:#000
style EHAL fill:#91e5a3,color:#000
style NOTE fill:#91e5a3,color:#000
全局分配器设置
alloc crate 提供 Vec、String、Box — 但你需要告诉 Rust 堆内存从何而来。这相当于为你的平台实现 malloc():
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use alloc::string::String;
use embedded_alloc::LlffHeap as Heap;
#[global_allocator]
static HEAP: Heap = Heap::empty();
#[cortex_m_rt::entry]
fn main() -> ! {
// Initialize the allocator with a memory region
// (typically a portion of RAM not used by stack or static data)
{
const HEAP_SIZE: usize = 4096;
static mut HEAP_MEM: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
// SAFETY: HEAP_MEM is only accessed here during init, before any allocation.
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
}
// Now you can use heap types!
let mut log_buffer: Vec<u8> = Vec::with_capacity(256);
let name: String = String::from("sensor_01");
// ...
loop {}
}
| C 堆设置 | Rust 等价 |
|---|---|
_sbrk() / 自定义 malloc() | #[global_allocator] + Heap::init() |
configTOTAL_HEAP_SIZE(FreeRTOS) | HEAP_SIZE 常量 |
pvPortMalloc() | alloc::vec::Vec::new() — 自动 |
| 堆耗尽 → 未定义行为 | alloc_error_handler → 可控 panic |
混合 no_std + std 工作区
真实项目(如大型 Rust 工作区)通常包含:
no_std库 crate,用于硬件无关逻辑std二进制 crate,用于 Linux 应用层
workspace_root/
├── Cargo.toml # [workspace] members = [...]
├── protocol/ # no_std — wire protocol, parsing
│ ├── Cargo.toml # no default-features, no std
│ └── src/lib.rs # #![no_std]
├── driver/ # no_std — hardware abstraction
│ ├── Cargo.toml
│ └── src/lib.rs # #![no_std], uses embedded-hal traits
├── firmware/ # no_std — MCU binary
│ ├── Cargo.toml # depends on protocol, driver
│ └── src/main.rs # #![no_std] #![no_main]
└── host_tool/ # std — Linux CLI tool
├── Cargo.toml # depends on protocol (same crate!)
└── src/main.rs # Uses std::fs, std::net, etc.
关键模式:protocol crate 使用 #![no_std],因此可同时为 MCU 固件和 Linux 主机工具编译。共享代码,零重复。
# protocol/Cargo.toml
[package]
name = "protocol"
[features]
default = []
std = [] # Optional: enable std-specific features when building for host
[dependencies]
serde = { version = "1", default-features = false, features = ["derive"] }
# Note: default-features = false drops serde's std dependency
#![allow(unused)]
fn main() {
// protocol/src/lib.rs
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "std")]
extern crate std;
extern crate alloc;
use alloc::vec::Vec;
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct DiagPacket {
pub sensor_id: u16,
pub value: i32,
pub fault_code: u16,
}
// This function works in both no_std and std contexts
pub fn parse_packet(data: &[u8]) -> Result<DiagPacket, &'static str> {
if data.len() < 8 {
return Err("packet too short");
}
Ok(DiagPacket {
sensor_id: u16::from_le_bytes([data[0], data[1]]),
value: i32::from_le_bytes([data[2], data[3], data[4], data[5]]),
fault_code: u16::from_le_bytes([data[6], data[7]]),
})
}
}
练习:硬件抽象层驱动
为通过 SPI 通信的假想 LED 控制器编写 no_std 驱动。驱动应泛型化于任意使用 embedded-hal 的 SPI 实现。
要求:
- 定义
LedController<SPI>结构体 - 实现
new()、set_brightness(led: u8, brightness: u8)和all_off() - SPI 协议:发送 2 字节事务
[led_index, brightness_value] - 使用 mock SPI 实现编写测试
#![allow(unused)]
fn main() {
// Starter code
#![no_std]
use embedded_hal::spi::SpiDevice;
pub struct LedController<SPI> {
spi: SPI,
num_leds: u8,
}
// TODO: Implement new(), set_brightness(), all_off()
// TODO: Create MockSpi for testing
}
解答(点击展开)
#![allow(unused)]
#![no_std]
fn main() {
use embedded_hal::spi::SpiDevice;
pub struct LedController<SPI> {
spi: SPI,
num_leds: u8,
}
impl<SPI: SpiDevice> LedController<SPI> {
pub fn new(spi: SPI, num_leds: u8) -> Self {
Self { spi, num_leds }
}
pub fn set_brightness(&mut self, led: u8, brightness: u8) -> Result<(), SPI::Error> {
if led >= self.num_leds {
return Ok(()); // Silently ignore out-of-range LEDs
}
self.spi.write(&[led, brightness])
}
pub fn all_off(&mut self) -> Result<(), SPI::Error> {
for led in 0..self.num_leds {
self.spi.write(&[led, 0])?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
// Mock SPI that records all transactions
struct MockSpi {
transactions: Vec<Vec<u8>>,
}
// Minimal error type for mock
#[derive(Debug)]
struct MockError;
impl embedded_hal::spi::Error for MockError {
fn kind(&self) -> embedded_hal::spi::ErrorKind {
embedded_hal::spi::ErrorKind::Other
}
}
impl embedded_hal::spi::ErrorType for MockSpi {
type Error = MockError;
}
impl SpiDevice for MockSpi {
fn write(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
self.transactions.push(buf.to_vec());
Ok(())
}
fn read(&mut self, _buf: &mut [u8]) -> Result<(), Self::Error> { Ok(()) }
fn transfer(&mut self, _r: &mut [u8], _w: &[u8]) -> Result<(), Self::Error> { Ok(()) }
fn transfer_in_place(&mut self, _buf: &mut [u8]) -> Result<(), Self::Error> { Ok(()) }
fn transaction(&mut self, _ops: &mut [embedded_hal::spi::Operation<'_, u8>]) -> Result<(), Self::Error> { Ok(()) }
}
#[test]
fn test_set_brightness() {
let mock = MockSpi { transactions: vec![] };
let mut ctrl = LedController::new(mock, 4);
ctrl.set_brightness(2, 128).unwrap();
assert_eq!(ctrl.spi.transactions, vec![vec![2, 128]]);
}
#[test]
fn test_all_off() {
let mock = MockSpi { transactions: vec![] };
let mut ctrl = LedController::new(mock, 3);
ctrl.all_off().unwrap();
assert_eq!(ctrl.spi.transactions, vec![
vec![0, 0], vec![1, 0], vec![2, 0],
]);
}
#[test]
fn test_out_of_range_led() {
let mock = MockSpi { transactions: vec![] };
let mut ctrl = LedController::new(mock, 2);
ctrl.set_brightness(5, 255).unwrap(); // Out of range — ignored
assert!(ctrl.spi.transactions.is_empty());
}
}
}
嵌入式 Rust 调试 — probe-rs、defmt 与 VS Code
C 固件开发者通常用 OpenOCD + GDB 或厂商 IDE(Keil、IAR、Segger Ozone)调试。Rust 嵌入式生态已收敛到 probe-rs 作为统一调试探针接口,用单一 Rust 原生工具替代 OpenOCD + GDB 栈。
probe-rs — 一体化调试探针工具
probe-rs 替代 OpenOCD + GDB 组合。开箱即支持 CMSIS-DAP、ST-Link、J-Link 等调试探针:
# Install probe-rs (includes cargo-flash and cargo-embed)
cargo install probe-rs-tools
# Flash and run your firmware
cargo flash --chip STM32F401RE --release
# Flash, run, and open RTT (Real-Time Transfer) console
cargo embed --chip STM32F401RE
probe-rs 与 OpenOCD + GDB:
| 方面 | OpenOCD + GDB | probe-rs |
|---|---|---|
| 安装 | 2 个独立包 + 脚本 | cargo install probe-rs-tools |
| 配置 | 每板/探针的 .cfg 文件 | --chip 标志或 Embed.toml |
| 控制台输出 | Semihosting(很慢) | RTT(约快 10 倍) |
| 日志框架 | printf | defmt(结构化、零成本) |
| 烧录算法 | XML pack 文件 | 内置 1000+ 芯片 |
| GDB 支持 | 原生 | probe-rs gdb 适配器 |
Embed.toml — 项目配置
probe-rs 使用单一配置文件,替代 .cfg 与 .gdbinit:
# Embed.toml — placed in your project root
[default.general]
chip = "STM32F401RETx"
[default.rtt]
enabled = true # Enable Real-Time Transfer console
channels = [
{ up = 0, mode = "BlockIfFull", name = "Terminal" },
]
[default.flashing]
enabled = true # Flash before running
restore_unwritten_bytes = false
[default.reset]
halt_afterwards = false # Start running after flash + reset
[default.gdb]
enabled = false # Set true to expose GDB server on :1337
gdb_connection_string = "127.0.0.1:1337"
# With Embed.toml, just run:
cargo embed # Flash + RTT console — zero flags needed
cargo embed --release # Release build
defmt — 嵌入式日志的延迟格式化
defmt(deferred formatting)替代 printf 调试。格式字符串存放在 ELF 文件中,而非 flash — 目标端日志调用仅发送索引 + 参数字节。这使日志比 printf 快 10–100 倍,且占用 flash 空间极少:
#![no_std]
#![no_main]
use defmt::{info, warn, error, debug, trace};
use defmt_rtt as _; // RTT transport — links the defmt output to probe-rs
#[cortex_m_rt::entry]
fn main() -> ! {
info!("Boot complete, firmware v{}", env!("CARGO_PKG_VERSION"));
let sensor_id: u16 = 0x4A;
let temperature: f32 = 23.5;
// Format strings stay in ELF, not flash — near-zero overhead
debug!("Sensor {:#06X}: {:.1}°C", sensor_id, temperature);
if temperature > 80.0 {
warn!("Overtemp on sensor {:#06X}: {:.1}°C", sensor_id, temperature);
}
loop {
cortex_m::asm::wfi(); // Wait for interrupt
}
}
// Custom types — derive defmt::Format instead of Debug
#[derive(defmt::Format)]
struct SensorReading {
id: u16,
value: i32,
status: SensorStatus,
}
#[derive(defmt::Format)]
enum SensorStatus {
Ok,
Warning,
Fault(u8),
}
// Usage:
// info!("Reading: {:?}", reading); // <-- uses defmt::Format, NOT std Debug
defmt 与 printf、log:
| 特性 | C printf(semihosting) | Rust log crate | defmt |
|---|---|---|---|
| 速度 | 每次调用约 100ms | N/A(需要 std) | 每次调用约 1μs |
| Flash 占用 | 完整格式字符串 | 完整格式字符串 | 仅索引(字节) |
| 传输 | Semihosting(暂停 CPU) | 串口/UART | RTT(非阻塞) |
| 结构化输出 | 否 | 仅文本 | 类型化、二进制编码 |
no_std | 通过 semihosting | 仅门面(后端需要 std) | ✅ 原生 |
| 过滤级别 | 手动 #ifdef | RUST_LOG=debug | defmt::println + features |
VS Code 调试配置
配合 probe-rs VS Code 扩展,可获得完整图形化调试 — 断点、变量检查、调用栈与寄存器视图:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "probe-rs-debug",
"request": "launch",
"name": "Flash & Debug (probe-rs)",
"chip": "STM32F401RETx",
"coreConfigs": [
{
"programBinary": "target/thumbv7em-none-eabihf/debug/${workspaceFolderBasename}",
"rttEnabled": true,
"rttChannelFormats": [
{
"channelNumber": 0,
"dataFormat": "Defmt",
"showTimestamps": true
}
]
}
],
"connectUnderReset": true,
"speed": 4000
}
]
}
安装扩展:
#![allow(unused)]
fn main() {
ext install probe-rs.probe-rs-debugger
}
C 调试工作流与 Rust 嵌入式调试
graph LR
subgraph "C Workflow (Traditional)"
C1["Write code"] --> C2["make flash"]
C2 --> C3["openocd -f board.cfg"]
C3 --> C4["arm-none-eabi-gdb<br/>target remote :3333"]
C4 --> C5["printf via semihosting<br/>(~100ms per call, halts CPU)"]
end
subgraph "Rust Workflow (probe-rs)"
R1["Write code"] --> R2["cargo embed"]
R2 --> R3["Flash + RTT console<br/>in one command"]
R3 --> R4["defmt logs stream<br/>in real-time (~1μs)"]
R2 -.->|"Or"| R5["VS Code F5<br/>Full GUI debugger"]
end
style C5 fill:#ffa07a,color:#000
style R3 fill:#91e5a3,color:#000
style R4 fill:#91e5a3,color:#000
style R5 fill:#91e5a3,color:#000
| C 调试操作 | Rust 等价 |
|---|---|
openocd -f board/st_nucleo_f4.cfg | probe-rs info(自动检测探针 + 芯片) |
arm-none-eabi-gdb -x .gdbinit | probe-rs gdb --chip STM32F401RE |
target remote :3333 | GDB 连接 localhost:1337 |
monitor reset halt | probe-rs reset --chip ... |
load firmware.elf | cargo flash --chip ... |
printf("debug: %d\n", val)(semihosting) | defmt::info!("debug: {}", val)(RTT) |
| Keil/IAR GUI 调试器 | VS Code + probe-rs-debugger 扩展 |
| Segger SystemView | defmt + probe-rs RTT 查看器 |
交叉引用:嵌入式驱动中使用的进阶 unsafe 模式(pin projection、自定义 arena/slab 分配器),请参阅配套 Rust Patterns 指南中的 “Pin Projections — Structural Pinning” 与 “Custom Allocators — Arena and Slab Patterns” 章节。
案例研究概览:C++ 到 Rust 的翻译
你将学到: 将约 10 万行 C++ 翻译为约 20 个 crate、约 9 万行 Rust 的真实项目经验。五个关键转换模式及其背后的架构决策。
- 我们将大型 C++ 诊断系统(约 10 万行 C++)翻译为 Rust 实现(约 20 个 Rust crate、约 9 万行)
- 本节展示实际使用的模式 — 不是玩具示例,而是真实生产代码
- 五个关键转换:
| # | C++ 模式 | Rust 模式 | 影响 |
|---|---|---|---|
| 1 | 类层次 + dynamic_cast | 枚举分发 + match | 约 400 → 0 次 dynamic_cast |
| 2 | shared_ptr / enable_shared_from_this 树 | Arena + 索引链接 | 无引用循环 |
| 3 | 每个模块中的 Framework* 裸指针 | 带生命周期借用的 DiagContext<'a> | 编译期有效性 |
| 4 | God object | 可组合状态结构体 | 可测试、模块化 |
| 5 | 处处 vector<unique_ptr<Base>> | 仅在需要处使用 Trait 对象(约 25 处) | 默认静态分发 |
前后指标
| 指标 | C++(原始) | Rust(重写) |
|---|---|---|
dynamic_cast / 类型向下转型 | 约 400 | 0 |
virtual / override 方法 | 约 900 | 约 25(Box<dyn Trait>) |
裸 new 分配 | 约 200 | 0(全部为 owned 类型) |
shared_ptr / 引用计数 | 约 10(拓扑库) | 0(仅在 FFI 边界使用 Arc) |
enum class 定义 | 约 60 | 约 190 个 pub enum |
| 模式匹配表达式 | N/A | 约 750 个 match |
| God object(>5K 行) | 2 | 0 |
案例研究 1:继承层次 → 枚举分发
C++ 模式:事件类层次
// C++ original: Every GPU event type is a class inheriting from GpuEventBase
class GpuEventBase {
public:
virtual ~GpuEventBase() = default;
virtual void Process(DiagFramework* fw) = 0;
uint16_t m_recordId;
uint8_t m_sensorType;
// ... common fields
};
class GpuPcieDegradeEvent : public GpuEventBase {
public:
void Process(DiagFramework* fw) override;
uint8_t m_linkSpeed;
uint8_t m_linkWidth;
};
class GpuPcieFatalEvent : public GpuEventBase { /* ... */ };
class GpuBootEvent : public GpuEventBase { /* ... */ };
// ... 10+ event classes inheriting from GpuEventBase
// Processing requires dynamic_cast:
void ProcessEvents(std::vector<std::unique_ptr<GpuEventBase>>& events,
DiagFramework* fw) {
for (auto& event : events) {
if (auto* degrade = dynamic_cast<GpuPcieDegradeEvent*>(event.get())) {
// handle degrade...
} else if (auto* fatal = dynamic_cast<GpuPcieFatalEvent*>(event.get())) {
// handle fatal...
}
// ... 10 more branches
}
}
Rust 方案:枚举分发
#![allow(unused)]
fn main() {
// Example: types.rs — No inheritance, no vtable, no dynamic_cast
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GpuEventKind {
PcieDegrade,
PcieFatal,
PcieUncorr,
Boot,
BaseboardState,
EccError,
OverTemp,
PowerRail,
ErotStatus,
Unknown,
}
}
#![allow(unused)]
fn main() {
// Example: manager.rs — Separate typed Vecs, no downcasting needed
pub struct GpuEventManager {
sku: SkuVariant,
degrade_events: Vec<GpuPcieDegradeEvent>, // Concrete type, not Box<dyn>
fatal_events: Vec<GpuPcieFatalEvent>,
uncorr_events: Vec<GpuPcieUncorrEvent>,
boot_events: Vec<GpuBootEvent>,
baseboard_events: Vec<GpuBaseboardEvent>,
ecc_events: Vec<GpuEccEvent>,
// ... each event type gets its own Vec
}
// Accessors return typed slices — zero ambiguity
impl GpuEventManager {
pub fn degrade_events(&self) -> &[GpuPcieDegradeEvent] {
&self.degrade_events
}
pub fn fatal_events(&self) -> &[GpuPcieFatalEvent] {
&self.fatal_events
}
}
}
为何不用 Vec<Box<dyn GpuEvent>>?
- 错误做法(字面翻译):把所有事件放进一个异构集合再向下转型 — 这正是 C++ 用
vector<unique_ptr<Base>>做的事 - 正确做法:分离的类型化
Vec消除所有向下转型。每个消费者只索取它需要的事件类型 - 性能:分离的
Vec缓存局部性更好(所有 degrade 事件在内存中连续)
案例研究 2:shared_ptr 树 → Arena/索引模式
C++ 模式:引用计数树
// C++ topology library: PcieDevice uses enable_shared_from_this
// because parent and child nodes both need to reference each other
class PcieDevice : public std::enable_shared_from_this<PcieDevice> {
public:
std::shared_ptr<PcieDevice> m_upstream;
std::vector<std::shared_ptr<PcieDevice>> m_downstream;
// ... device data
void AddChild(std::shared_ptr<PcieDevice> child) {
child->m_upstream = shared_from_this(); // Parent ↔ child cycle!
m_downstream.push_back(child);
}
};
// Problem: parent→child and child→parent create reference cycles
// Need weak_ptr to break cycles, but easy to forget
Rust 方案:Arena 与索引链接
#![allow(unused)]
fn main() {
// Example: components.rs — Flat Vec owns all devices
pub struct PcieDevice {
pub base: PcieDeviceBase,
pub kind: PcieDeviceKind,
// Tree linkage via indices — no reference counting, no cycles
pub upstream_idx: Option<usize>, // Index into the arena Vec
pub downstream_idxs: Vec<usize>, // Indices into the arena Vec
}
// The "arena" is simply a Vec<PcieDevice> owned by the tree:
pub struct DeviceTree {
devices: Vec<PcieDevice>, // Flat ownership — one Vec owns everything
}
impl DeviceTree {
pub fn parent(&self, device_idx: usize) -> Option<&PcieDevice> {
self.devices[device_idx].upstream_idx
.map(|idx| &self.devices[idx])
}
pub fn children(&self, device_idx: usize) -> Vec<&PcieDevice> {
self.devices[device_idx].downstream_idxs
.iter()
.map(|&idx| &self.devices[idx])
.collect()
}
}
}
关键洞察
- 无
shared_ptr、无weak_ptr、无enable_shared_from_this - 不可能出现引用循环 — 索引只是
usize值 - 更好的缓存性能 — 所有设备在连续内存中
- 更简单的推理 — 单一所有者(
Vec),多个观察者(索引)
graph LR
subgraph "C++ shared_ptr Tree"
A1["shared_ptr<Device>"] -->|"shared_ptr"| B1["shared_ptr<Device>"]
B1 -->|"shared_ptr (parent)"| A1
A1 -->|"shared_ptr"| C1["shared_ptr<Device>"]
C1 -->|"shared_ptr (parent)"| A1
style A1 fill:#ff6b6b,color:#000
style B1 fill:#ffa07a,color:#000
style C1 fill:#ffa07a,color:#000
end
subgraph "Rust Arena + Index"
V["Vec<PcieDevice>"]
V --> D0["[0] Root<br/>upstream: None<br/>down: [1,2]"]
V --> D1["[1] Child<br/>upstream: Some(0)<br/>down: []"]
V --> D2["[2] Child<br/>upstream: Some(0)<br/>down: []"]
style V fill:#51cf66,color:#000
style D0 fill:#91e5a3,color:#000
style D1 fill:#91e5a3,color:#000
style D2 fill:#91e5a3,color:#000
end
案例研究 3:框架通信 → 生命周期借用
你将学到: 如何将 C++ 裸指针框架通信模式转换为 Rust 基于生命周期的借用系统,在保持零成本抽象的同时消除悬垂指针风险。
C++ 模式:指向框架的裸指针
// C++ original: Every diagnostic module stores a raw pointer to the framework
class DiagBase {
protected:
DiagFramework* m_pFramework; // Raw pointer — who owns this?
public:
DiagBase(DiagFramework* fw) : m_pFramework(fw) {}
void LogEvent(uint32_t code, const std::string& msg) {
m_pFramework->GetEventLog()->Record(code, msg); // Hope it's still alive!
}
};
// Problem: m_pFramework is a raw pointer with no lifetime guarantee
// If framework is destroyed while modules still reference it → UB
Rust 方案:带生命周期借用的 DiagContext
#![allow(unused)]
fn main() {
// Example: module.rs — Borrow, don't store
/// Context passed to diagnostic modules during execution.
/// The lifetime 'a guarantees the framework outlives the context.
pub struct DiagContext<'a> {
pub der_log: &'a mut EventLogManager,
pub config: &'a ModuleConfig,
pub framework_opts: &'a HashMap<String, String>,
}
/// Modules receive context as a parameter — never store framework pointers
pub trait DiagModule {
fn id(&self) -> &str;
fn execute(&mut self, ctx: &mut DiagContext) -> DiagResult<()>;
fn pre_execute(&mut self, _ctx: &mut DiagContext) -> DiagResult<()> {
Ok(())
}
fn post_execute(&mut self, _ctx: &mut DiagContext) -> DiagResult<()> {
Ok(())
}
}
}
关键洞察
- C++ 模块存储指向框架的指针(危险:框架先被销毁怎么办?)
- Rust 模块接收上下文作为函数参数 — 借用检查器保证调用期间框架仍然存活
- 无裸指针、无生命周期歧义、无需「希望它还活着」
案例研究 4:God object → 可组合状态
C++ 模式:单体框架类
// C++ original: The framework is god object
class DiagFramework {
// Health-monitor trap processing
std::vector<AlertTriggerInfo> m_alertTriggers;
std::vector<WarnTriggerInfo> m_warnTriggers;
bool m_healthMonHasBootTimeError;
uint32_t m_healthMonActionCounter;
// GPU diagnostics
std::map<uint32_t, GpuPcieInfo> m_gpuPcieMap;
bool m_isRecoveryContext;
bool m_healthcheckDetectedDevices;
// ... 30+ more GPU-related fields
// PCIe tree
std::shared_ptr<CPcieTreeLinux> m_pPcieTree;
// Event logging
CEventLogMgr* m_pEventLogMgr;
// ... several other methods
void HandleGpuEvents();
void HandleNicEvents();
void RunGpuDiag();
// Everything depends on everything
};
Rust 方案:可组合状态结构体
#![allow(unused)]
fn main() {
// Example: main.rs — State decomposed into focused structs
#[derive(Default)]
struct HealthMonitorState {
alert_triggers: Vec<AlertTriggerInfo>,
warn_triggers: Vec<WarnTriggerInfo>,
health_monitor_action_counter: u32,
health_monitor_has_boot_time_error: bool,
// Only health-monitor-related fields
}
#[derive(Default)]
struct GpuDiagState {
gpu_pcie_map: HashMap<u32, GpuPcieInfo>,
is_recovery_context: bool,
healthcheck_detected_devices: bool,
// Only GPU-related fields
}
/// The framework composes these states rather than owning everything flat
struct DiagFramework {
ctx: DiagContext, // Execution context
args: Args, // CLI arguments
pcie_tree: Option<DeviceTree>, // No shared_ptr needed
event_log_mgr: EventLogManager, // Owned, not raw pointer
fc_manager: FcManager, // Fault code management
health: HealthMonitorState, // Health-monitor state — its own struct
gpu: GpuDiagState, // GPU state — its own struct
}
}
关键洞察
- 可测试性:每个状态结构体可独立单元测试
- 可读性:
self.health.alert_triggersvsm_alertTriggers— 所有权清晰 - 放心重构:修改
GpuDiagState不会意外影响健康监控处理 - 无方法堆砌:只需健康监控状态的函数接受
&mut HealthMonitorState,而非整个框架
案例研究 5:Trait 对象 — 何时才合适
- 并非一切都应是枚举!诊断模块插件系统是 Trait 对象的正当用例
- 原因:诊断模块对扩展开放 — 可添加新模块而无需修改框架
#![allow(unused)]
fn main() {
// Example: framework.rs — Vec<Box<dyn DiagModule>> is correct here
pub struct DiagFramework {
modules: Vec<Box<dyn DiagModule>>, // Runtime polymorphism
pre_diag_modules: Vec<Box<dyn DiagModule>>,
event_log_mgr: EventLogManager,
// ...
}
impl DiagFramework {
/// Register a diagnostic module — any type implementing DiagModule
pub fn register_module(&mut self, module: Box<dyn DiagModule>) {
info!("Registering module: {}", module.id());
self.modules.push(module);
}
}
}
何时使用哪种模式
| 用例 | 模式 | 原因 |
|---|---|---|
| 编译期已知的固定变体集合 | enum + match | 穷尽检查、无 vtable |
| 硬件事件类型(Degrade、Fatal、Boot 等) | enum GpuEventKind | 所有变体已知,性能重要 |
| PCIe 设备类型(GPU、NIC、Switch 等) | enum PcieDeviceKind | 固定集合,各变体数据不同 |
| 插件/模块系统(对扩展开放) | Box<dyn Trait> | 无需修改框架即可添加新模块 |
| 测试 mock | Box<dyn Trait> | 注入测试替身 |
练习:翻译前先思考
给定以下 C++ 代码:
class Shape { public: virtual double area() = 0; };
class Circle : public Shape { double r; double area() override { return 3.14*r*r; } };
class Rect : public Shape { double w, h; double area() override { return w*h; } };
std::vector<std::unique_ptr<Shape>> shapes;
问题:Rust 翻译应使用 enum Shape 还是 Vec<Box<dyn Shape>>?
解答(点击展开)
答案:enum Shape — 因为形状集合是封闭的(编译期已知)。仅当用户可在运行时添加新形状类型时,才使用 Box<dyn Shape>。
// Correct Rust translation:
enum Shape {
Circle { r: f64 },
Rect { w: f64, h: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { r } => std::f64::consts::PI * r * r,
Shape::Rect { w, h } => w * h,
}
}
}
fn main() {
let shapes: Vec<Shape> = vec![
Shape::Circle { r: 5.0 },
Shape::Rect { w: 3.0, h: 4.0 },
];
for shape in &shapes {
println!("Area: {:.2}", shape.area());
}
}
// Output:
// Area: 78.54
// Area: 12.00
翻译指标与经验总结
我们学到了什么
- 默认使用枚举分发 — 在约 10 万行 C++ 中,仅约 25 处
Box<dyn Trait>真正必要(插件系统、测试 mock)。其余约 900 个虚方法变为带match的枚举 - Arena 模式消除引用循环 —
shared_ptr与enable_shared_from_this是所有权不清的症状。先想清楚谁拥有数据 - 传递上下文,不要存储指针 — 生命周期有界的
DiagContext<'a>比在每个模块里存Framework*更安全、更清晰 - 分解 god object — 若结构体有 30+ 字段,多半是 3–4 个结构体穿同一件大衣
- 编译器是你的结对伙伴 — 约 400 次
dynamic_cast意味着约 400 次潜在运行时失败。Rust 中零dynamic_cast等价物意味着零运行时类型错误
最难的部分
- 生命周期注解:习惯了裸指针后,把借用写对需要时间 — 但一旦编译通过,就是对的
- 与借用检查器较劲:想在两处同时持有
&mut self。解法:将状态分解为独立结构体 - 抵制字面翻译:处处写
Vec<Box<dyn Base>>的诱惑。问自己:「这组变体是封闭的吗?」→ 若是,用枚举
给 C++ 团队的建议
- 从小而独立的模块开始(不要先动 god object)
- 先翻译数据结构,再翻译行为
- 让编译器引导你 — 错误信息非常出色
- 在
dyn Trait之前先考虑enum - 用 Rust playground 在集成前原型验证模式
Rust 最佳实践摘要
你将学到: 编写地道 Rust 的实用指南 — 代码组织、命名约定、错误处理模式与文档。你会经常回顾的快速参考章节。
代码组织
- 优先小函数:易于测试与推理
- 使用描述性名称:
calculate_total_price()而非calc() - 将相关功能分组:使用模块与独立文件
- 编写文档:对公共 API 使用
///
错误处理
- 除非确定不会 panic,避免
unwrap():仅在 100% 确信时使用
#![allow(unused)]
fn main() {
// Bad: Can panic
let value = some_option.unwrap();
// Good: Handle the None case
let value = some_option.unwrap_or(default_value);
let value = some_option.unwrap_or_else(|| expensive_computation());
let value = some_option.unwrap_or_default(); // Uses Default trait
// For Result<T, E>
let value = some_result.unwrap_or(fallback_value);
let value = some_result.unwrap_or_else(|err| {
eprintln!("Error occurred: {err}");
default_value
});
}
- 用描述性消息使用
expect():当 unwrap 合理时,说明原因
#![allow(unused)]
fn main() {
let config = std::env::var("CONFIG_PATH")
.expect("CONFIG_PATH environment variable must be set");
}
- 可失败操作返回
Result<T, E>:让调用方决定如何处理错误 - 自定义错误类型使用
thiserror:比手写实现更顺手
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {message}")]
Parse { message: String },
#[error("Value {value} is out of range")]
OutOfRange { value: i32 },
}
}
- 用
?运算符链式传播错误:沿调用栈向上传递 - 优先
thiserror而非anyhow:我们团队约定用#[derive(thiserror::Error)]定义显式错误枚举,以便调用方匹配具体变体。anyhow::Error适合快速原型,但会抹平错误类型,使调用方难以处理特定失败。库与生产代码用thiserror;仅在只需打印错误的临时脚本或顶层二进制中用anyhow。 - 何时
unwrap()可接受:- 单元测试:
assert_eq!(result.unwrap(), expected) - 原型:会被替换的临时代码
- 不会失败的操作:能证明不会失败时
- 单元测试:
#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3];
let first = numbers.get(0).unwrap(); // Safe: we just created the vec with elements
// Better: Use expect() with explanation
let first = numbers.get(0).expect("numbers vec is non-empty by construction");
}
- 快速失败:尽早检查前置条件并立即返回错误
内存管理
- 优先借用而非 clone:尽可能用
&T代替 clone - 谨慎使用
Rc<T>:仅在需要共享所有权时 - 限制生命周期:用作用域
{}控制值何时 drop - 公共 API 避免
RefCell<T>:内部可变性保持内部
性能
- 先 profile 再优化:使用
cargo bench与 profiling 工具 - 优先迭代器而非循环:更可读且往往更快
- 需要所有权时用
&str而非String - 大栈对象考虑
Box<T>:必要时移到堆上
应实现的基本 Trait
每种类型都应考虑的核心 Trait
创建自定义类型时,考虑实现这些基础 Trait,使类型在 Rust 中感觉原生:
Debug 与 Display
#![allow(unused)]
fn main() {
use std::fmt;
#[derive(Debug)] // Automatic implementation for debugging
struct Person {
name: String,
age: u32,
}
// Manual Display implementation for user-facing output
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (age {})", self.name, self.age)
}
}
// Usage:
let person = Person { name: "Alice".to_string(), age: 30 };
println!("{:?}", person); // Debug: Person { name: "Alice", age: 30 }
println!("{}", person); // Display: Alice (age 30)
}
Clone 与 Copy
#![allow(unused)]
fn main() {
// Copy: Implicit duplication for small, simple types
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
// Clone: Explicit duplication for complex types
#[derive(Debug, Clone)]
struct Person {
name: String, // String doesn't implement Copy
age: u32,
}
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy (implicit)
let person1 = Person { name: "Bob".to_string(), age: 25 };
let person2 = person1.clone(); // Clone (explicit)
}
PartialEq 与 Eq
#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq)]
struct UserId(u64);
#[derive(Debug, PartialEq)]
struct Temperature {
celsius: f64, // f64 doesn't implement Eq (due to NaN)
}
let id1 = UserId(123);
let id2 = UserId(123);
assert_eq!(id1, id2); // Works because of PartialEq
let temp1 = Temperature { celsius: 20.0 };
let temp2 = Temperature { celsius: 20.0 };
assert_eq!(temp1, temp2); // Works with PartialEq
}
PartialOrd 与 Ord
#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Priority(u8);
let high = Priority(1);
let low = Priority(10);
assert!(high < low); // Lower numbers = higher priority
// Use in collections
let mut priorities = vec![Priority(5), Priority(1), Priority(8)];
priorities.sort(); // Works because Priority implements Ord
}
Default
#![allow(unused)]
fn main() {
#[derive(Debug, Default)]
struct Config {
debug: bool, // false (default)
max_connections: u32, // 0 (default)
timeout: Option<u64>, // None (default)
}
// Custom Default implementation
impl Default for Config {
fn default() -> Self {
Config {
debug: false,
max_connections: 100, // Custom default
timeout: Some(30), // Custom default
}
}
}
let config = Config::default();
let config = Config { debug: true, ..Default::default() }; // Partial override
}
From 与 Into
#![allow(unused)]
fn main() {
struct UserId(u64);
struct UserName(String);
// Implement From, and Into comes for free
impl From<u64> for UserId {
fn from(id: u64) -> Self {
UserId(id)
}
}
impl From<String> for UserName {
fn from(name: String) -> Self {
UserName(name)
}
}
impl From<&str> for UserName {
fn from(name: &str) -> Self {
UserName(name.to_string())
}
}
// Usage:
let user_id: UserId = 123u64.into(); // Using Into
let user_id = UserId::from(123u64); // Using From
let username = UserName::from("alice"); // &str -> UserName
let username: UserName = "bob".into(); // Using Into
}
TryFrom 与 TryInto
#![allow(unused)]
fn main() {
use std::convert::TryFrom;
struct PositiveNumber(u32);
#[derive(Debug)]
struct NegativeNumberError;
impl TryFrom<i32> for PositiveNumber {
type Error = NegativeNumberError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value >= 0 {
Ok(PositiveNumber(value as u32))
} else {
Err(NegativeNumberError)
}
}
}
// Usage:
let positive = PositiveNumber::try_from(42)?; // Ok(PositiveNumber(42))
let error = PositiveNumber::try_from(-5); // Err(NegativeNumberError)
}
Serde(序列化)
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}
// Automatic JSON serialization/deserialization
let user = User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
let json = serde_json::to_string(&user)?;
let deserialized: User = serde_json::from_str(&json)?;
}
Trait 实现清单
对新类型,可参考此清单:
#![allow(unused)]
fn main() {
#[derive(
Debug, // [OK] Always implement for debugging
Clone, // [OK] If the type should be duplicatable
PartialEq, // [OK] If the type should be comparable
Eq, // [OK] If comparison is reflexive/transitive
PartialOrd, // [OK] If the type has ordering
Ord, // [OK] If ordering is total
Hash, // [OK] If type will be used as HashMap key
Default, // [OK] If there's a sensible default value
)]
struct MyType {
// fields...
}
// Manual implementations to consider:
impl Display for MyType { /* user-facing representation */ }
impl From<OtherType> for MyType { /* convenient conversion */ }
impl TryFrom<FallibleType> for MyType { /* fallible conversion */ }
}
何时不应实现 Trait
- 堆数据类型不要实现 Copy:
String、Vec、HashMap等 - 值可能为 NaN 时不要实现 Eq:含
f32/f64的类型 - 没有合理默认值时不要实现 Default:文件句柄、网络连接
- clone 昂贵时不要实现 Clone:大型数据结构(考虑
Rc<T>)
摘要:Trait 收益
| Trait | 收益 | 何时使用 |
|---|---|---|
Debug | println!("{:?}", value) | 几乎总是(极少数例外) |
Display | println!("{}", value) | 面向用户的类型 |
Clone | value.clone() | 显式复制有意义时 |
Copy | 隐式复制 | 小型简单类型 |
PartialEq | == 与 != | 大多数类型 |
Eq | 自反相等 | 数学上相等可靠时 |
PartialOrd | <、>、<=、>= | 有自然序的类型 |
Ord | .sort()、BinaryHeap | 全序时 |
Hash | HashMap 键 | 用作 map 键的类型 |
Default | Default::default() | 有明显默认值的类型 |
From/Into | 便捷转换 | 常见类型转换 |
TryFrom/TryInto | 可失败转换 | 可能失败的转换 |
避免过度 clone()
避免过度 clone()
你将学到: 为何
.clone()在 Rust 中是代码异味、如何通过重构所有权消除不必要拷贝,以及标志所有权设计问题的具体模式。
- 来自 C++ 时,
.clone()感觉像安全默认 — 「复制一下就行」。但过度 clone 掩盖所有权问题并损害性能。 - 经验法则:若 clone 是为了满足借用检查器,多半需要重构所有权而非复制。
何时 clone() 是错误的
#![allow(unused)]
fn main() {
// BAD: Cloning a String just to pass it to a function that only reads it
fn log_message(msg: String) { // Takes ownership unnecessarily
println!("[LOG] {}", msg);
}
let message = String::from("GPU test passed");
log_message(message.clone()); // Wasteful: allocates a whole new String
log_message(message); // Original consumed — clone was pointless
}
#![allow(unused)]
fn main() {
// GOOD: Accept a borrow — zero allocation
fn log_message(msg: &str) { // Borrows, doesn't own
println!("[LOG] {}", msg);
}
let message = String::from("GPU test passed");
log_message(&message); // No clone, no allocation
log_message(&message); // Can call again — message not consumed
}
真实示例:返回 &str 而非 clone
#![allow(unused)]
fn main() {
// Example: healthcheck.rs — returns a borrowed view, zero allocation
pub fn serial_or_unknown(&self) -> &str {
self.serial.as_deref().unwrap_or(UNKNOWN_VALUE)
}
pub fn model_or_unknown(&self) -> &str {
self.model.as_deref().unwrap_or(UNKNOWN_VALUE)
}
}
C++ 等价物会返回 const std::string& 或 std::string_view — 但 C++ 中两者都没有生命周期检查。Rust 中借用检查器保证返回的 &str 不会比 self 活得更久。
真实示例:静态字符串切片 — 完全不用堆
#![allow(unused)]
fn main() {
// Example: healthcheck.rs — compile-time string tables
const HBM_SCREEN_RECIPES: &[&str] = &[
"hbm_ds_ntd", "hbm_ds_ntd_gfx", "hbm_dt_ntd", "hbm_dt_ntd_gfx",
"hbm_burnin_8h", "hbm_burnin_24h",
];
}
C++ 中通常是 std::vector<std::string>(首次使用时堆分配)。Rust 的 &'static [&'static str] 存在于只读内存 — 零运行时成本。
何时 clone() 是合适的
| 情况 | 为何 clone 可接受 | 示例 |
|---|---|---|
线程间 Arc::clone() | 仅增加引用计数(约 1 ns),不复制数据 | let flag = stop_flag.clone(); |
| 将数据移入 spawn 的线程 | 线程需要自己的副本 | let ctx = ctx.clone(); thread::spawn(move || { ... }) |
从 &self 字段取出 | 不能从借用中移出 | 返回 owned String 时 self.name.clone() |
包在 Option 中的小 Copy 类型 | .copied() 比 .clone() 更清晰 | Option<&u32> → Option<u32> 用 opt.get(0).copied() |
真实示例:线程共享的 Arc::clone
#![allow(unused)]
fn main() {
// Example: workload.rs — Arc::clone is cheap (ref count bump)
let stop_flag = Arc::new(AtomicBool::new(false));
let stop_flag_clone = stop_flag.clone(); // ~1 ns, no data copied
let ctx_clone = ctx.clone(); // Clone context for move into thread
let sensor_handle = thread::spawn(move || {
// ...uses stop_flag_clone and ctx_clone
});
}
清单:我该 clone 吗?
- 能否接受
&str/&T而非String/T? → 借用,不要 clone - 能否重构以避免两个所有者? → 传引用或用作用域
- 这是
Arc::clone()吗? → 可以,O(1) - 要把数据移入线程/闭包? → clone 必要
- 在热循环里 clone? → profile,考虑借用或
Cow<T>
Cow<'a, T>:写时克隆 — 能借则借,必须时才 clone
Cow(Clone on Write)是枚举,持有借用引用或owned 值。相当于「尽量免分配,修改时才分配」。C++ 没有直接等价物 — 最接近的是有时返回 const std::string&、有时返回 std::string 的函数。
为何需要 Cow
#![allow(unused)]
fn main() {
// Without Cow — you must choose: always borrow OR always clone
fn normalize(s: &str) -> String { // Always allocates!
if s.contains(' ') {
s.replace(' ', "_") // New String (allocation needed)
} else {
s.to_string() // Unnecessary allocation!
}
}
// With Cow — borrow when unchanged, allocate only when modified
use std::borrow::Cow;
fn normalize(s: &str) -> Cow<'_, str> {
if s.contains(' ') {
Cow::Owned(s.replace(' ', "_")) // Allocates (must modify)
} else {
Cow::Borrowed(s) // Zero allocation (passthrough)
}
}
}
Cow 如何工作
use std::borrow::Cow;
// Cow<'a, str> is essentially:
// enum Cow<'a, str> {
// Borrowed(&'a str), // Zero-cost reference
// Owned(String), // Heap-allocated owned value
// }
fn greet(name: &str) -> Cow<'_, str> {
if name.is_empty() {
Cow::Borrowed("stranger") // Static string — no allocation
} else if name.starts_with(' ') {
Cow::Owned(name.trim().to_string()) // Modified — allocation needed
} else {
Cow::Borrowed(name) // Passthrough — no allocation
}
}
fn main() {
let g1 = greet("Alice"); // Cow::Borrowed("Alice")
let g2 = greet(""); // Cow::Borrowed("stranger")
let g3 = greet(" Bob "); // Cow::Owned("Bob")
// Cow<str> implements Deref<Target = str>, so you can use it as &str:
println!("Hello, {g1}!"); // Works — Cow auto-derefs to &str
println!("Hello, {g2}!");
println!("Hello, {g3}!");
}
真实用例:配置值规范化
use std::borrow::Cow;
/// Normalize a SKU name: trim whitespace, lowercase.
/// Returns Cow::Borrowed if already normalized (zero allocation).
fn normalize_sku(sku: &str) -> Cow<'_, str> {
let trimmed = sku.trim();
if trimmed == sku && sku.chars().all(|c| c.is_lowercase() || !c.is_alphabetic()) {
Cow::Borrowed(sku) // Already normalized — no allocation
} else {
Cow::Owned(trimmed.to_lowercase()) // Needs modification — allocate
}
}
fn main() {
let s1 = normalize_sku("server-x1"); // Borrowed — zero alloc
let s2 = normalize_sku(" Server-X1 "); // Owned — must allocate
println!("{s1}, {s2}"); // "server-x1, server-x1"
}
何时使用 Cow
| 情况 | 用 Cow? |
|---|---|
| 函数大多原样返回输入 | ✅ 是 — 避免不必要 clone |
| 解析/规范化字符串(trim、小写、替换) | ✅ 是 — 输入常已合法 |
| 总是修改 — 每条路径都分配 | ❌ 否 — 直接返回 String |
| 简单透传(从不修改) | ❌ 否 — 直接返回 &str |
| 长期存在结构体中的数据 | ❌ 否 — 用 String(owned) |
C++ 对比:
Cow<str>类似返回std::variant<std::string_view, std::string>的函数 — 但有自动 deref,访问值无样板代码。
Weak<T>:打破引用循环 — Rust 的 weak_ptr
Weak<T> 是 C++ std::weak_ptr<T> 的 Rust 等价物。它持有对 Rc<T> 或 Arc<T> 的非 owning 引用。值可在仍有 Weak 引用时被释放 — 调用 upgrade() 若值已消失则返回 None。
为何需要 Weak
Rc<T> 与 Arc<T> 若两个值相互指向,会形成引用循环 — 两者引用计数永不为 0,都不会被 drop(内存泄漏)。Weak 打破循环:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: String,
parent: RefCell<Weak<Node>>, // Weak — doesn't prevent parent from dropping
children: RefCell<Vec<Rc<Node>>>, // Strong — parent owns children
}
impl Node {
fn new(value: &str) -> Rc<Node> {
Rc::new(Node {
value: value.to_string(),
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
})
}
fn add_child(parent: &Rc<Node>, child: &Rc<Node>) {
// Child gets a weak reference to parent (no cycle)
*child.parent.borrow_mut() = Rc::downgrade(parent);
// Parent gets a strong reference to child
parent.children.borrow_mut().push(Rc::clone(child));
}
}
fn main() {
let root = Node::new("root");
let child = Node::new("child");
Node::add_child(&root, &child);
// Access parent from child via upgrade()
if let Some(parent) = child.parent.borrow().upgrade() {
println!("Child's parent: {}", parent.value); // "root"
}
println!("Root strong count: {}", Rc::strong_count(&root)); // 1
println!("Root weak count: {}", Rc::weak_count(&root)); // 1
}
C++ 对比
// C++ — weak_ptr to break shared_ptr cycle
struct Node {
std::string value;
std::weak_ptr<Node> parent; // Weak — no ownership
std::vector<std::shared_ptr<Node>> children; // Strong — owns children
static auto create(const std::string& v) {
return std::make_shared<Node>(Node{v, {}, {}});
}
};
auto root = Node::create("root");
auto child = Node::create("child");
child->parent = root; // weak_ptr assignment
root->children.push_back(child);
if (auto p = child->parent.lock()) { // lock() → shared_ptr or null
std::cout << "Parent: " << p->value << std::endl;
}
| C++ | Rust | 说明 |
|---|---|---|
shared_ptr<T> | Rc<T>(单线程)/ Arc<T>(多线程) | 语义相同 |
weak_ptr<T> | Weak<T> from Rc::downgrade() / Arc::downgrade() | 语义相同 |
weak_ptr::lock() → shared_ptr 或 null | Weak::upgrade() → Option<Rc<T>> | 已 drop 则为 None |
shared_ptr::use_count() | Rc::strong_count() | 含义相同 |
何时使用 Weak
| 情况 | 模式 |
|---|---|
| 父子树关系 | 父持 Rc<Child>,子持 Weak<Parent> |
| 观察者模式 / 事件监听器 | 事件源持 Weak<Observer>,观察者持 Rc<Source> |
| 不阻止释放的缓存 | HashMap<Key, Weak<Value>> — 条目自然过期 |
| 图结构中的打破循环 | 交叉链接用 Weak,树边用 Rc/Arc |
新代码中优先 arena 模式(案例研究 2)而非
Rc/Weak处理树结构。Vec<T>+ 索引更简单、更快,无引用计数开销。需要动态生命周期的共享所有权时才用Rc/Weak。
Copy vs Clone、PartialEq vs Eq — 何时 derive 什么
- Copy ≈ C++ 可平凡复制(无自定义拷贝构造/析构)。 如
int、枚举、简单 POD 结构体 — 编译器自动生成按位memcpy。Rust 中Copy同理:赋值let b = a;隐式按位复制,两变量仍有效。 - Clone ≈ C++ 拷贝构造 /
operator=深拷贝。 C++ 类有自定义拷贝构造(如深拷贝std::vector成员)时,Rust 等价是实现Clone。必须显式.clone()— Rust 不会在=后隐藏昂贵拷贝。 - 关键区别: C++ 中平凡拷贝与深拷贝都通过同一
=语法隐式发生。Rust 强制选择:Copy类型静默复制(廉价),非Copy默认移动,昂贵重复须用.clone()显式选择。 - 同理,C++
operator==不区分a == a恒真的类型(如整数)与不恒真的类型(如带 NaN 的float)。Rust 用PartialEqvsEq编码这一点。
Copy vs Clone
| Copy | Clone | |
|---|---|---|
| 如何工作 | 按位 memcpy(隐式) | 自定义逻辑(显式 .clone()) |
| 何时发生 | 赋值:let b = a; | 仅调用 .clone() 时 |
| 复制/clone 后 | a 与 b 均有效 | a 与 b 均有效 |
| 既无 Copy 也无 Clone | let b = a; 移动 a(a 消失) | let b = a; 移动 a(a 消失) |
| 允许用于 | 无堆数据的类型 | 任意类型 |
| C++ 类比 | 可平凡复制 / POD(无自定义拷贝构造) | 自定义拷贝构造(深拷贝) |
真实示例:Copy — 简单枚举
#![allow(unused)]
fn main() {
// From fan_diag/src/sensor.rs — all unit variants, fits in 1 byte
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum FanStatus {
#[default]
Normal,
Low,
High,
Missing,
Failed,
Unknown,
}
let status = FanStatus::Normal;
let copy = status; // Implicit copy — status is still valid
println!("{:?} {:?}", status, copy); // Both work
}
真实示例:Copy — 带整数载荷的枚举
#![allow(unused)]
fn main() {
// Example: healthcheck.rs — u32 payloads are Copy, so the whole enum is too
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthcheckStatus {
Pass,
ProgramError(u32),
DmesgError(u32),
RasError(u32),
OtherError(u32),
Unknown,
}
}
真实示例:仅 Clone — 含堆数据的结构体
#![allow(unused)]
fn main() {
// Example: components.rs — String prevents Copy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FruData {
pub technology: DeviceTechnology,
pub physical_location: String, // ← String: heap-allocated, can't Copy
pub expected: bool,
pub removable: bool,
}
// let a = fru_data; → MOVES (a is gone)
// let a = fru_data.clone(); → CLONES (fru_data still valid, new heap allocation)
}
规则:能否 Copy?
Does the type contain String, Vec, Box, HashMap,
Rc, Arc, or any other heap-owning type?
YES → Clone only (cannot be Copy)
NO → You CAN derive Copy (and should, if the type is small)
PartialEq vs Eq
| PartialEq | Eq | |
|---|---|---|
| 提供什么 | == 与 != | 标记:「相等是自反的」 |
| 自反?(a == a) | 不保证 | 保证 |
| 为何重要 | f32::NAN != f32::NAN | HashMap 键要求 Eq |
| 何时 derive | 几乎总是 | 类型无 f32/f64 字段时 |
| C++ 类比 | operator== | 无直接等价(C++ 不检查) |
真实示例:Eq — 用作 HashMap 键
#![allow(unused)]
fn main() {
// From hms_trap/src/cpu_handler.rs — Hash requires Eq
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CpuFaultType {
InvalidFaultType,
CpuCperFatalErr,
CpuLpddr5UceErr,
CpuC2CUceFatalErr,
// ...
}
// Used as: HashMap<CpuFaultType, FaultHandler>
// HashMap keys must be Eq + Hash — PartialEq alone won't compile
}
真实示例:无法 Eq — 类型含 f32
#![allow(unused)]
fn main() {
// Example: types.rs — f32 prevents Eq
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TemperatureSensors {
pub warning_threshold: Option<f32>, // ← f32 has NaN ≠ NaN
pub critical_threshold: Option<f32>, // ← can't derive Eq
pub sensor_names: Vec<String>,
}
// Cannot be used as HashMap key. Cannot derive Eq.
// Because: f32::NAN == f32::NAN is false, violating reflexivity.
}
PartialOrd vs Ord
| PartialOrd | Ord | |
|---|---|---|
| 提供什么 | <、>、<=、>= | .sort()、BTreeMap 键 |
| 全序? | 否(某些对可能不可比) | 是(每对都可比) |
| f32/f64? | 仅 PartialOrd(NaN 破坏序) | 不能 derive Ord |
真实示例:Ord — 严重级别排序
#![allow(unused)]
fn main() {
// From hms_trap/src/fault.rs — variant order defines severity
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FaultSeverity {
Info, // lowest (discriminant 0)
Warning, // (discriminant 1)
Error, // (discriminant 2)
Critical, // highest (discriminant 3)
}
// FaultSeverity::Info < FaultSeverity::Critical → true
// Enables: if severity >= FaultSeverity::Error { escalate(); }
}
真实示例:Ord — 诊断级别比较
#![allow(unused)]
fn main() {
// Example: orchestration.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum GpuDiagLevel {
#[default]
Quick, // lowest
Standard,
Extended,
Full, // highest
}
// Enables: if requested_level >= GpuDiagLevel::Extended { run_extended_tests(); }
}
Derive 决策树
Your new type
│
Contains String/Vec/Box?
/ \
YES NO
│ │
Clone only Clone + Copy
│ │
Contains f32/f64? Contains f32/f64?
/ \ / \
YES NO YES NO
│ │ │ │
PartialEq PartialEq PartialEq PartialEq
only + Eq only + Eq
│ │
Need sorting? Need sorting?
/ \ / \
YES NO YES NO
│ │ │ │
PartialOrd Done PartialOrd Done
+ Ord + Ord
│ │
Need as Need as
map key? map key?
│ │
+ Hash + Hash
快速参考:生产 Rust 中的常见 derive 组合
| 类型类别 | 典型 derive | 示例 |
|---|---|---|
| 简单状态枚举 | Copy, Clone, PartialEq, Eq, Default | FanStatus |
| 用作 HashMap 键的枚举 | Copy, Clone, PartialEq, Eq, Hash | CpuFaultType、SelComponent |
| 可排序严重级别枚举 | Copy, Clone, PartialEq, Eq, PartialOrd, Ord | FaultSeverity、GpuDiagLevel |
| 含 String 的数据结构体 | Clone, Debug, Serialize, Deserialize | FruData、OverallSummary |
| 可序列化配置 | Clone, Debug, Default, Serialize, Deserialize | DiagConfig |
避免未检查索引
避免未检查索引
你将学到: 为何在 Rust 中
vec[i]很危险(越界会 panic),以及.get()、迭代器、HashMap的entry()API 等安全替代方案。用显式处理取代 C++ 的未定义行为。
- 在 C++ 中,
vec[i]和map[key]可能未定义行为 / 在键缺失时自动插入。Rust 的[]在越界时会 panic。 - 规则:除非能证明索引有效,否则使用
.get()而非[]。
C++ → Rust 对比
// C++ — silent UB or insertion
std::vector<int> v = {1, 2, 3};
int x = v[10]; // UB! No bounds check with operator[]
std::map<std::string, int> m;
int y = m["missing"]; // Silently inserts key with value 0!
#![allow(unused)]
fn main() {
// Rust — safe alternatives
let v = vec![1, 2, 3];
// Bad: panics if index out of bounds
// let x = v[10];
// Good: returns Option<&i32>
let x = v.get(10); // None — no panic
let x = v.get(1).copied().unwrap_or(0); // 2, or 0 if missing
}
真实示例:生产 Rust 代码中的安全字节解析
#![allow(unused)]
fn main() {
// Example: diagnostics.rs
// Parsing a binary SEL record — buffer might be shorter than expected
let sensor_num = bytes.get(7).copied().unwrap_or(0);
let ppin = cpu_ppin.get(i).map(|s| s.as_str()).unwrap_or("");
}
真实示例:用 .and_then() 链式安全查找
#![allow(unused)]
fn main() {
// Example: profile.rs — double lookup: HashMap → Vec
pub fn get_processor(&self, location: &str) -> Option<&Processor> {
self.processor_by_location
.get(location) // HashMap → Option<&usize>
.and_then(|&idx| self.processors.get(idx)) // Vec → Option<&Processor>
}
// Both lookups return Option — no panics, no UB
}
真实示例:安全 JSON 导航
#![allow(unused)]
fn main() {
// Example: framework.rs — every JSON key returns Option
let manufacturer = product_fru
.get("Manufacturer") // Option<&Value>
.and_then(|v| v.as_str()) // Option<&str>
.unwrap_or(UNKNOWN_VALUE) // &str (safe fallback)
.to_string();
}
对比 C++ 模式:json["SystemInfo"]["ProductFru"]["Manufacturer"] — 任一缺失键都会抛出 nlohmann::json::out_of_range。
何时 [] 可接受
- 边界检查之后:
if i < v.len() { v[i] } - 测试中:希望 panic 时
- 常量索引:在
assert!(!v.is_empty());之后let first = v[0];
使用 unwrap_or 安全提取值
unwrap()在None/Err时会 panic。生产代码中优先使用安全替代方案。
unwrap 系列方法
| 方法 | 在 None/Err 时的行为 | 适用场景 |
|---|---|---|
.unwrap() | Panic | 仅测试,或可证明不会失败 |
.expect("msg") | 带消息的 panic | panic 合理时,说明原因 |
.unwrap_or(default) | 返回 default | 有廉价的常量回退值 |
.unwrap_or_else(|| expr) | 调用闭包 | 回退值计算成本高 |
.unwrap_or_default() | 返回 Default::default() | 类型实现了 Default |
真实示例:带安全默认值的解析
#![allow(unused)]
fn main() {
// Example: peripherals.rs
// Regex capture groups might not match — provide safe fallbacks
let bus_hex = caps.get(1).map(|m| m.as_str()).unwrap_or("00");
let fw_status = caps.get(5).map(|m| m.as_str()).unwrap_or("0x0");
let bus = u8::from_str_radix(bus_hex, 16).unwrap_or(0);
}
真实示例:用 unwrap_or_else 回退到结构体
#![allow(unused)]
fn main() {
// Example: framework.rs
// Full function wraps logic in an Option-returning closure;
// if anything fails, return a default struct:
(|| -> Option<BaseboardFru> {
let content = std::fs::read_to_string(path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
// ... extract fields with .get()? chains
Some(baseboard_fru)
})()
.unwrap_or_else(|| BaseboardFru {
manufacturer: String::new(),
model: String::new(),
product_part_number: String::new(),
serial_number: String::new(),
asset_tag: String::new(),
})
}
真实示例:配置反序列化中的 unwrap_or_default
#![allow(unused)]
fn main() {
// Example: framework.rs
// If JSON config parsing fails, fall back to Default — no crash
Ok(json) => serde_json::from_str(&json).unwrap_or_default(),
}
C++ 等价物是在 nlohmann::json::parse() 外包 try/catch,在 catch 块中手动构造默认值。
函数式变换:map、map_err、find_map
Option和Result上的这些方法让你在不 unwrap 的情况下变换内部值,用线性链替代嵌套if/else。
速查表
| 方法 | 作用于 | 作用 | C++ 等价 |
|---|---|---|---|
.map(|v| ...) | Option / Result | 变换 Some/Ok 中的值 | if (opt) { *opt = transform(*opt); } |
.map_err(|e| ...) | Result | 变换 Err 中的值 | 在 catch 块中添加上下文 |
.and_then(|v| ...) | Option / Result | 链接返回 Option/Result 的操作 | 嵌套 if 检查 |
.find_map(|v| ...) | Iterator | 一次遍历完成 find + map | 带 if + break 的循环 |
.filter(|v| ...) | Option / Iterator | 只保留满足谓词的值 | if (!predicate) return nullopt; |
.ok()? | Result | 将 Result → Option 并传播 None | if (result.has_error()) return nullopt; |
真实示例:用 .and_then() 链提取 JSON 字段
#![allow(unused)]
fn main() {
// Example: framework.rs — finding serial number with fallbacks
let sys_info = json.get("SystemInfo")?;
// Try BaseboardFru.BoardSerialNumber first
if let Some(serial) = sys_info
.get("BaseboardFru")
.and_then(|b| b.get("BoardSerialNumber"))
.and_then(|v| v.as_str())
.filter(valid_serial) // Only accept non-empty, valid serials
{
return Some(serial.to_string());
}
// Fallback to BoardFru.SerialNumber
sys_info
.get("BoardFru")
.and_then(|b| b.get("SerialNumber"))
.and_then(|v| v.as_str())
.filter(valid_serial)
.map(|s| s.to_string()) // Convert &str → String only if Some
}
在 C++ 中这会是 if (json.contains("BaseboardFru")) { if (json["BaseboardFru"].contains("BoardSerialNumber")) { ... } } 这样的金字塔。
真实示例:find_map — 一次遍历完成搜索与变换
#![allow(unused)]
fn main() {
// Example: context.rs — find SDR record matching sensor + owner
pub fn find_for_event(&self, sensor_number: u8, owner_id: u8) -> Option<&SdrRecord> {
self.by_sensor.get(&sensor_number).and_then(|indices| {
indices.iter().find_map(|&i| {
let record = &self.records[i];
if record.sensor_owner_id() == Some(owner_id) {
Some(record)
} else {
None
}
})
})
}
}
find_map 融合了 find 与 map:在第一个匹配处停止并变换。C++ 等价物是带 if + break 的 for 循环。
真实示例:用 map_err 添加上下文
#![allow(unused)]
fn main() {
// Example: main.rs — add context to errors before propagating
let json_str = serde_json::to_string_pretty(&config)
.map_err(|e| format!("Failed to serialize config: {}", e))?;
}
将 serde_json::Error 变换为包含失败原因上下文的描述性 String 错误。
JSON 处理:nlohmann::json → serde
- C++ 团队通常用
nlohmann::json解析 JSON。Rust 使用 serde + serde_json — 更强大,因为 JSON 模式编码在类型系统中。
C++(nlohmann)与 Rust(serde)对比
// C++ with nlohmann::json — runtime field access
#include <nlohmann/json.hpp>
using json = nlohmann::json;
struct Fan {
std::string logical_id;
std::vector<std::string> sensor_ids;
};
Fan parse_fan(const json& j) {
Fan f;
f.logical_id = j.at("LogicalID").get<std::string>(); // throws if missing
if (j.contains("SDRSensorIdHexes")) { // manual default handling
f.sensor_ids = j["SDRSensorIdHexes"].get<std::vector<std::string>>();
}
return f;
}
#![allow(unused)]
fn main() {
// Rust with serde — compile-time schema, automatic field mapping
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fan {
pub logical_id: String,
#[serde(rename = "SDRSensorIdHexes", default)] // JSON key → Rust field
pub sensor_ids: Vec<String>, // Missing → empty Vec
#[serde(default)]
pub sensor_names: Vec<String>, // Missing → empty Vec
}
// One line replaces the entire parse function:
let fan: Fan = serde_json::from_str(json_str)?;
}
关键 serde 属性(生产 Rust 代码中的真实示例)
| 属性 | 用途 | C++ 等价 |
|---|---|---|
#[serde(default)] | 缺失字段使用 Default::default() | if (j.contains(key)) { ... } else { default; } |
#[serde(rename = "Key")] | 将 JSON 键名映射到 Rust 字段名 | 手动 j.at("Key") 访问 |
#[serde(flatten)] | 将未知键吸收进 HashMap | for (auto& [k,v] : j.items()) { ... } |
#[serde(skip)] | 不序列化/反序列化该字段 | 不存入 JSON |
#[serde(tag = "type")] | 内部标记枚举(判别字段) | if (j["type"] == "gpu") { ... } |
真实示例:完整配置结构体
#![allow(unused)]
fn main() {
// Example: diag.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiagConfig {
pub sku: SkuConfig,
#[serde(default)]
pub level: DiagLevel, // Missing → DiagLevel::default()
#[serde(default)]
pub modules: ModuleConfig, // Missing → ModuleConfig::default()
#[serde(default)]
pub output_dir: String, // Missing → ""
#[serde(default, flatten)]
pub options: HashMap<String, serde_json::Value>, // Absorbs unknown keys
}
// Loading is 3 lines (vs ~20+ in C++ with nlohmann):
let content = std::fs::read_to_string(path)?;
let config: DiagConfig = serde_json::from_str(&content)?;
Ok(config)
}
用 #[serde(tag = "type")] 反序列化枚举
#![allow(unused)]
fn main() {
// Example: components.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")] // JSON: {"type": "Gpu", "product": ...}
pub enum PcieDeviceKind {
Gpu { product: GpuProduct, manufacturer: GpuManufacturer },
Nic { product: NicProduct, manufacturer: NicManufacturer },
NvmeDrive { drive_type: StorageDriveType, capacity_gb: u32 },
// ... 9 more variants
}
// serde automatically dispatches on the "type" field — no manual if/else chain
}
C++ 等价物是:if (j["type"] == "Gpu") { parse_gpu(j); } else if (j["type"] == "Nic") { parse_nic(j); } ...
练习:用 serde 进行 JSON 反序列化
- 定义可从以下 JSON 反序列化的
ServerConfig结构体:
{
"hostname": "diag-node-01",
"port": 8080,
"debug": true,
"modules": ["accel_diag", "nic_diag", "cpu_diag"]
}
- 使用
#[derive(Deserialize)]和serde_json::from_str()解析 - 为
debug添加#[serde(default)],缺失时默认为false - 加分项:添加带
#[serde(default)]的enum DiagLevel { Quick, Full, Extended }字段,默认为Quick
起始代码(需要 cargo add serde --features derive 和 cargo add serde_json):
use serde::Deserialize;
// TODO: Define DiagLevel enum with Default impl
// TODO: Define ServerConfig struct with serde attributes
fn main() {
let json_input = r#"{
"hostname": "diag-node-01",
"port": 8080,
"debug": true,
"modules": ["accel_diag", "nic_diag", "cpu_diag"]
}"#;
// TODO: Deserialize and print the config
// TODO: Try parsing JSON with "debug" field missing — verify it defaults to false
}
解答(点击展开)
use serde::Deserialize;
#[derive(Debug, Deserialize, Default)]
enum DiagLevel {
#[default]
Quick,
Full,
Extended,
}
#[derive(Debug, Deserialize)]
struct ServerConfig {
hostname: String,
port: u16,
#[serde(default)] // defaults to false if missing
debug: bool,
modules: Vec<String>,
#[serde(default)] // defaults to DiagLevel::Quick if missing
level: DiagLevel,
}
fn main() {
let json_input = r#"{
"hostname": "diag-node-01",
"port": 8080,
"debug": true,
"modules": ["accel_diag", "nic_diag", "cpu_diag"]
}"#;
let config: ServerConfig = serde_json::from_str(json_input)
.expect("Failed to parse JSON");
println!("{config:#?}");
// Test with missing optional fields
let minimal = r#"{
"hostname": "node-02",
"port": 9090,
"modules": []
}"#;
let config2: ServerConfig = serde_json::from_str(minimal)
.expect("Failed to parse minimal JSON");
println!("debug (default): {}", config2.debug); // false
println!("level (default): {:?}", config2.level); // Quick
}
// Output:
// ServerConfig {
// hostname: "diag-node-01",
// port: 8080,
// debug: true,
// modules: ["accel_diag", "nic_diag", "cpu_diag"],
// level: Quick,
// }
// debug (default): false
// level (default): Quick
折叠赋值金字塔
用闭包折叠赋值金字塔
你将学到: Rust 基于表达式的语法与闭包如何将 C++ 中深层嵌套的
if/else校验链压平为清晰、线性的代码。
- C++ 在赋值变量时常需要多分支
if/else链,尤其在涉及校验或回退逻辑时。Rust 基于表达式的语法与闭包将这些压平为扁平、线性的代码。
模式 1:用 if 表达式进行元组赋值
// C++ — three variables set across a multi-block if/else chain
uint32_t fault_code;
const char* der_marker;
const char* action;
if (is_c44ad) {
fault_code = 32709; der_marker = "CSI_WARN"; action = "No action";
} else if (error.is_hardware_error()) {
fault_code = 67956; der_marker = "CSI_ERR"; action = "Replace GPU";
} else {
fault_code = 32709; der_marker = "CSI_WARN"; action = "No action";
}
#![allow(unused)]
fn main() {
// Rust equivalent:accel_fieldiag.rs
// Single expression assigns all three at once:
let (fault_code, der_marker, recommended_action) = if is_c44ad {
(32709u32, "CSI_WARN", "No action")
} else if error.is_hardware_error() {
(67956u32, "CSI_ERR", "Replace GPU")
} else {
(32709u32, "CSI_WARN", "No action")
};
}
模式 2:IIFE(立即调用函数表达式)处理可失败链
// C++ — pyramid of doom for JSON navigation
std::string get_part_number(const nlohmann::json& root) {
if (root.contains("SystemInfo")) {
auto& sys = root["SystemInfo"];
if (sys.contains("BaseboardFru")) {
auto& bb = sys["BaseboardFru"];
if (bb.contains("ProductPartNumber")) {
return bb["ProductPartNumber"].get<std::string>();
}
}
}
return "UNKNOWN";
}
#![allow(unused)]
fn main() {
// Rust equivalent:framework.rs
// Closure + ? operator collapses the pyramid into linear code:
let part_number = (|| -> Option<String> {
let path = self.args.sysinfo.as_ref()?;
let content = std::fs::read_to_string(path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
let ppn = json
.get("SystemInfo")?
.get("BaseboardFru")?
.get("ProductPartNumber")?
.as_str()?;
Some(ppn.to_string())
})()
.unwrap_or_else(|| "UNKNOWN".to_string());
}
闭包创建一个 Option<String> 作用域,任一步失败时 ? 会提前退出。.unwrap_or_else() 在末尾一次性提供回退值。
模式 3:迭代器链替代手动循环 + push_back
// C++ — manual loop with intermediate variables
std::vector<std::tuple<std::vector<std::string>, std::string, std::string>> gpu_info;
for (const auto& [key, info] : gpu_pcie_map) {
std::vector<std::string> bdfs;
// ... parse bdf_path into bdfs
std::string serial = info.serial_number.value_or("UNKNOWN");
std::string model = info.model_number.value_or(model_name);
gpu_info.push_back({bdfs, serial, model});
}
#![allow(unused)]
fn main() {
// Rust equivalent:peripherals.rs
// Single chain: values() → map → collect
let gpu_info: Vec<(Vec<String>, String, String, String)> = self
.gpu_pcie_map
.values()
.map(|info| {
let bdfs: Vec<String> = info.bdf_path
.split(')')
.filter(|s| !s.is_empty())
.map(|s| s.trim_start_matches('(').to_string())
.collect();
let serial = info.serial_number.clone()
.unwrap_or_else(|| "UNKNOWN".to_string());
let model = info.model_number.clone()
.unwrap_or_else(|| model_name.to_string());
let gpu_bdf = format!("{}:{}:{}.{}",
info.bdf.segment, info.bdf.bus, info.bdf.device, info.bdf.function);
(bdfs, serial, model, gpu_bdf)
})
.collect();
}
模式 4:.filter().collect() 替代循环 + if (condition) continue
// C++
std::vector<TestResult*> failures;
for (auto& t : test_results) {
if (!t.is_pass()) {
failures.push_back(&t);
}
}
#![allow(unused)]
fn main() {
// Rust — from accel_diag/src/healthcheck.rs
pub fn failed_tests(&self) -> Vec<&TestResult> {
self.test_results.iter().filter(|t| !t.is_pass()).collect()
}
}
小结:何时使用各模式
| C++ 模式 | Rust 替代 | 主要收益 |
|---|---|---|
| 多分支变量赋值 | let (a, b) = if ... { } else { }; | 所有变量原子绑定 |
嵌套 if (contains) 金字塔 | 带 ? 的 IIFE 闭包 | 线性、扁平、早退 |
for 循环 + push_back | .iter().map(||).collect() | 无需中间可变 Vec |
for + if (cond) continue | .iter().filter(||).collect() | 声明式意图 |
for + if + break(找第一个) | .iter().find_map(||) | 一次遍历完成搜索与变换 |
综合练习:诊断事件流水线
🔴 挑战 — 综合练习,涵盖枚举、Trait、迭代器、错误处理与泛型
本综合练习整合枚举、Trait(特征)、迭代器、错误处理与泛型。你将构建一个简化的诊断事件处理流水线,类似生产 Rust 代码中的模式。
要求:
- 定义带
Display的enum Severity { Info, Warning, Critical },以及包含source: String、severity: Severity、message: String、fault_code: u32的struct DiagEvent - 定义带方法
fn should_include(&self, event: &DiagEvent) -> bool的trait EventFilter - 实现两个过滤器:
SeverityFilter(仅包含 >= 给定严重级别的事件)和SourceFilter(仅包含来自特定源字符串的事件) - 编写函数
fn process_events(events: &[DiagEvent], filters: &[&dyn EventFilter]) -> Vec<String>,返回通过全部过滤器的事件的格式化报告行 - 编写
fn parse_event(line: &str) -> Result<DiagEvent, String>,解析"source:severity:fault_code:message"形式的行(错误输入返回Err)
起始代码:
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Severity {
Info,
Warning,
Critical,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
todo!()
}
}
#[derive(Debug, Clone)]
struct DiagEvent {
source: String,
severity: Severity,
message: String,
fault_code: u32,
}
trait EventFilter {
fn should_include(&self, event: &DiagEvent) -> bool;
}
struct SeverityFilter {
min_severity: Severity,
}
// TODO: impl EventFilter for SeverityFilter
struct SourceFilter {
source: String,
}
// TODO: impl EventFilter for SourceFilter
fn process_events(events: &[DiagEvent], filters: &[&dyn EventFilter]) -> Vec<String> {
// TODO: Filter events that pass ALL filters, format as
// "[SEVERITY] source (FC:fault_code): message"
todo!()
}
fn parse_event(line: &str) -> Result<DiagEvent, String> {
// Parse "source:severity:fault_code:message"
// Return Err for invalid input
todo!()
}
fn main() {
let raw_lines = vec![
"accel_diag:Critical:67956:ECC uncorrectable error detected",
"nic_diag:Warning:32709:Link speed degraded",
"accel_diag:Info:10001:Self-test passed",
"cpu_diag:Critical:55012:Thermal throttling active",
"accel_diag:Warning:32710:PCIe link width reduced",
];
// Parse all lines, collect successes and report errors
let events: Vec<DiagEvent> = raw_lines.iter()
.filter_map(|line| match parse_event(line) {
Ok(e) => Some(e),
Err(e) => { eprintln!("Parse error: {e}"); None }
})
.collect();
// Apply filters: only Critical+Warning events from accel_diag
let sev_filter = SeverityFilter { min_severity: Severity::Warning };
let src_filter = SourceFilter { source: "accel_diag".to_string() };
let filters: Vec<&dyn EventFilter> = vec![&sev_filter, &src_filter];
let report = process_events(&events, &filters);
for line in &report {
println!("{line}");
}
println!("--- {} event(s) matched ---", report.len());
}
解答(点击展开)
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Severity {
Info,
Warning,
Critical,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Info => write!(f, "INFO"),
Severity::Warning => write!(f, "WARNING"),
Severity::Critical => write!(f, "CRITICAL"),
}
}
}
impl Severity {
fn from_str(s: &str) -> Result<Self, String> {
match s {
"Info" => Ok(Severity::Info),
"Warning" => Ok(Severity::Warning),
"Critical" => Ok(Severity::Critical),
other => Err(format!("Unknown severity: {other}")),
}
}
}
#[derive(Debug, Clone)]
struct DiagEvent {
source: String,
severity: Severity,
message: String,
fault_code: u32,
}
trait EventFilter {
fn should_include(&self, event: &DiagEvent) -> bool;
}
struct SeverityFilter {
min_severity: Severity,
}
impl EventFilter for SeverityFilter {
fn should_include(&self, event: &DiagEvent) -> bool {
event.severity >= self.min_severity
}
}
struct SourceFilter {
source: String,
}
impl EventFilter for SourceFilter {
fn should_include(&self, event: &DiagEvent) -> bool {
event.source == self.source
}
}
fn process_events(events: &[DiagEvent], filters: &[&dyn EventFilter]) -> Vec<String> {
events.iter()
.filter(|e| filters.iter().all(|f| f.should_include(e)))
.map(|e| format!("[{}] {} (FC:{}): {}", e.severity, e.source, e.fault_code, e.message))
.collect()
}
fn parse_event(line: &str) -> Result<DiagEvent, String> {
let parts: Vec<&str> = line.splitn(4, ':').collect();
if parts.len() != 4 {
return Err(format!("Expected 4 colon-separated fields, got {}", parts.len()));
}
let fault_code = parts[2].parse::<u32>()
.map_err(|e| format!("Invalid fault code '{}': {e}", parts[2]))?;
Ok(DiagEvent {
source: parts[0].to_string(),
severity: Severity::from_str(parts[1])?,
fault_code,
message: parts[3].to_string(),
})
}
fn main() {
let raw_lines = vec![
"accel_diag:Critical:67956:ECC uncorrectable error detected",
"nic_diag:Warning:32709:Link speed degraded",
"accel_diag:Info:10001:Self-test passed",
"cpu_diag:Critical:55012:Thermal throttling active",
"accel_diag:Warning:32710:PCIe link width reduced",
];
let events: Vec<DiagEvent> = raw_lines.iter()
.filter_map(|line| match parse_event(line) {
Ok(e) => Some(e),
Err(e) => { eprintln!("Parse error: {e}"); None }
})
.collect();
let sev_filter = SeverityFilter { min_severity: Severity::Warning };
let src_filter = SourceFilter { source: "accel_diag".to_string() };
let filters: Vec<&dyn EventFilter> = vec![&sev_filter, &src_filter];
let report = process_events(&events, &filters);
for line in &report {
println!("{line}");
}
println!("--- {} event(s) matched ---", report.len());
}
// Output:
// [CRITICAL] accel_diag (FC:67956): ECC uncorrectable error detected
// [WARNING] accel_diag (FC:32710): PCIe link width reduced
// --- 2 event(s) matched ---
日志与追踪生态
日志与追踪:syslog/printf → log + tracing
你将学到: Rust 的双层日志架构(门面 + 后端)、
log与tracingcrate、带 span 的结构化日志,以及如何替代printf/syslog调试。
C++ 诊断代码通常使用 printf、syslog 或自定义日志框架。
Rust 有标准化的双层日志架构:门面 crate(log 或
tracing)与后端(实际日志实现)。
log 门面 — Rust 通用日志 API
log crate 提供与 syslog 严重级别对应的宏。库使用
log 宏;可执行文件选择后端:
// Cargo.toml
// [dependencies]
// log = "0.4"
// env_logger = "0.11" # One of many backends
use log::{info, warn, error, debug, trace};
fn check_sensor(id: u32, temp: f64) {
trace!("Reading sensor {id}"); // Finest granularity
debug!("Sensor {id} raw value: {temp}"); // Development-time detail
if temp > 85.0 {
warn!("Sensor {id} high temperature: {temp}°C");
}
if temp > 95.0 {
error!("Sensor {id} CRITICAL: {temp}°C — initiating shutdown");
}
info!("Sensor {id} check complete"); // Normal operation
}
fn main() {
// Initialize the backend — typically done once in main()
env_logger::init(); // Controlled by RUST_LOG env var
check_sensor(0, 72.5);
check_sensor(1, 91.0);
}
# Control log level via environment variable
RUST_LOG=debug cargo run # Show debug and above
RUST_LOG=warn cargo run # Show only warn and error
RUST_LOG=my_crate=trace cargo run # Per-module filtering
RUST_LOG=my_crate::gpu=debug,warn cargo run # Mix levels
C++ 对比
| C++ | Rust(log) | 说明 |
|---|---|---|
printf("DEBUG: %s\n", msg) | debug!("{msg}") | 编译期检查格式 |
syslog(LOG_ERR, "...") | error!("...") | 由后端决定输出位置 |
日志调用外包 #ifdef DEBUG | 在 max_level 下 trace! / debug! 被编译掉 | 禁用时零成本 |
自定义 Logger::log(level, msg) | log::info!("...") — 所有 crate 同一 API | 通用门面,可换后端 |
| 按文件控制日志详细度 | RUST_LOG=crate::module=level | 基于环境变量,无需重编译 |
tracing crate — 带 span 的结构化日志
tracing 在 log 基础上扩展了结构化字段与 span(带时间的作用域)。
对需要跟踪上下文的诊断代码尤其有用:
// Cargo.toml
// [dependencies]
// tracing = "0.1"
// tracing-subscriber = { version = "0.3", features = ["env-filter"] }
use tracing::{info, warn, error, instrument, info_span};
#[instrument(skip(data), fields(gpu_id = gpu_id, data_len = data.len()))]
fn run_gpu_test(gpu_id: u32, data: &[u8]) -> Result<(), String> {
info!("Starting GPU test");
let span = info_span!("ecc_check", gpu_id);
let _guard = span.enter(); // All logs inside this scope include gpu_id
if data.is_empty() {
error!(gpu_id, "No test data provided");
return Err("empty data".to_string());
}
// Structured fields — machine-parseable, not just string interpolation
info!(
gpu_id,
temp_celsius = 72.5,
ecc_errors = 0,
"ECC check passed"
);
Ok(())
}
fn main() {
// Initialize tracing subscriber
tracing_subscriber::fmt()
.with_env_filter("debug") // Or use RUST_LOG env var
.with_target(true) // Show module path
.with_thread_ids(true) // Show thread IDs
.init();
let _ = run_gpu_test(0, &[1, 2, 3]);
}
使用 tracing-subscriber 时的输出:
#![allow(unused)]
fn main() {
2026-02-15T10:30:00.123Z DEBUG ThreadId(01) run_gpu_test{gpu_id=0 data_len=3}: my_crate: Starting GPU test
2026-02-15T10:30:00.124Z INFO ThreadId(01) run_gpu_test{gpu_id=0 data_len=3}:ecc_check{gpu_id=0}: my_crate: ECC check passed gpu_id=0 temp_celsius=72.5 ecc_errors=0
}
#[instrument] — 自动创建 span
#[instrument] 属性自动以函数名及其参数创建 span:
#![allow(unused)]
fn main() {
use tracing::instrument;
#[instrument]
fn parse_sel_record(record_id: u16, sensor_type: u8, data: &[u8]) -> Result<(), String> {
// Every log inside this function automatically includes:
// record_id, sensor_type, and data (if Debug)
tracing::debug!("Parsing SEL record");
Ok(())
}
// skip: exclude large/sensitive args from the span
// fields: add computed fields
#[instrument(skip(raw_buffer), fields(buf_len = raw_buffer.len()))]
fn decode_ipmi_response(raw_buffer: &[u8]) -> Result<Vec<u8>, String> {
tracing::trace!("Decoding {} bytes", raw_buffer.len());
Ok(raw_buffer.to_vec())
}
}
log 与 tracing — 选哪个
| 方面 | log | tracing |
|---|---|---|
| 复杂度 | 简单 — 5 个宏 | 更丰富 — span、字段、instrument |
| 结构化数据 | 仅字符串插值 | 键值字段:info!(gpu_id = 0, "msg") |
| 计时 / span | 无 | 有 — #[instrument]、span.enter() |
| 异步支持 | 基础 | 一等公民 — span 跨 .await 传播 |
| 兼容性 | 通用门面 | 与 log 兼容(有 log 桥接) |
| 适用场景 | 简单应用、库 | 诊断工具、异步代码、可观测性 |
建议:生产级诊断类项目(带结构化输出的诊断工具)用
tracing。 依赖要少的简单库用log。tracing含兼容层,使用log宏的库在tracingsubscriber 下仍可工作。
后端选项
| 后端 Crate | 输出 | 适用场景 |
|---|---|---|
env_logger | stderr,彩色 | 开发、简单 CLI 工具 |
tracing-subscriber | stderr,格式化 | 生产环境配合 tracing |
syslog | 系统 syslog | Linux 系统服务 |
tracing-journald | systemd journal | systemd 管理的服务 |
tracing-appender | 轮转日志文件 | 长期运行的守护进程 |
tracing-opentelemetry | OpenTelemetry 收集器 | 分布式追踪 |
18. C++ → Rust 语义深入
C++ → Rust 语义深入
你将学到: 对没有明显 Rust 等价物的 C++ 概念的详细映射 — 四种命名转换、SFINAE 与 trait 约束、CRTP 与关联类型,以及翻译过程中的其他常见摩擦点。
以下各节映射没有明显 1:1 Rust 等价物的 C++ 概念。这些差异常在 C++ 程序员做翻译工作时造成困扰。
转换层次:四种 C++ 转换 → Rust 等价物
C++ 有四种命名转换。Rust 用不同、更明确的机制替代它们:
// C++ casting hierarchy
int i = static_cast<int>(3.14); // 1. Numeric / up-cast
Derived* d = dynamic_cast<Derived*>(base); // 2. Runtime downcasting
int* p = const_cast<int*>(cp); // 3. Cast away const
auto* raw = reinterpret_cast<char*>(&obj); // 4. Bit-level reinterpretation
| C++ 转换 | Rust 等价 | 安全性 | 说明 |
|---|---|---|---|
static_cast(数值) | as 关键字 | 安全但可能截断/环绕 | let i = 3.14_f64 as i32; — 截断为 3 |
static_cast(数值,有检查) | From/Into | 安全,编译期验证 | let i: i32 = 42_u8.into(); — 仅拓宽 |
static_cast(数值,可失败) | TryFrom/TryInto | 安全,返回 Result | let i: u8 = 300_u16.try_into()?; — 返回 Err |
dynamic_cast(向下转换) | 对枚举 match / Any::downcast_ref | 安全 | 枚举用模式匹配;trait 对象用 Any |
const_cast | 无等价物 | Rust 在 Safe Rust 中无法将 & 转为 &mut。内部可变性用 Cell/RefCell | |
reinterpret_cast | std::mem::transmute | unsafe | 重新解释位模式。几乎总是错的 — 优先 from_le_bytes() 等 |
#![allow(unused)]
fn main() {
// Rust equivalents:
// 1. Numeric casts — prefer From/Into over `as`
let widened: u32 = 42_u8.into(); // Infallible widening — always prefer
let truncated = 300_u16 as u8; // ⚠ Wraps to 44! Silent data loss
let checked: Result<u8, _> = 300_u16.try_into(); // Err — safe fallible conversion
// 2. Downcast: enum (preferred) or Any (when needed for type erasure)
use std::any::Any;
fn handle_any(val: &dyn Any) {
if let Some(s) = val.downcast_ref::<String>() {
println!("Got string: {s}");
} else if let Some(n) = val.downcast_ref::<i32>() {
println!("Got int: {n}");
}
}
// 3. "const_cast" → interior mutability (no unsafe needed)
use std::cell::Cell;
struct Sensor {
read_count: Cell<u32>, // Mutate through &self
}
impl Sensor {
fn read(&self) -> f64 {
self.read_count.set(self.read_count.get() + 1); // &self, not &mut self
42.0
}
}
// 4. reinterpret_cast → transmute (almost never needed)
// Prefer safe alternatives:
let bytes: [u8; 4] = 0x12345678_u32.to_ne_bytes(); // ✅ Safe
let val = u32::from_ne_bytes(bytes); // ✅ Safe
// unsafe { std::mem::transmute::<u32, [u8; 4]>(val) } // ❌ Avoid
}
准则:惯用 Rust 中,
as应少见(拓宽用From/Into, 收窄用TryFrom/TryInto),transmute应极少见,const_cast无等价物,因为内部可变性 类型使其不必要。
预处理器 → cfg、特性标志与 macro_rules!
C++ 大量依赖预处理器做条件编译、常量与代码生成。Rust 用一等语言特性全部替代。
#define 常量 → const 或 const fn
// C++
#define MAX_RETRIES 5
#define BUFFER_SIZE (1024 * 64)
#define SQUARE(x) ((x) * (x)) // Macro — textual substitution, no type safety
#![allow(unused)]
fn main() {
// Rust — type-safe, scoped, no textual substitution
const MAX_RETRIES: u32 = 5;
const BUFFER_SIZE: usize = 1024 * 64;
const fn square(x: u32) -> u32 { x * x } // Evaluated at compile time
// Can be used in const contexts:
const AREA: u32 = square(12); // Computed at compile time
static BUFFER: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
}
#ifdef / #if → #[cfg()] 与 cfg!()
// C++
#ifdef DEBUG
log_verbose("Step 1 complete");
#endif
#if defined(LINUX) && !defined(ARM)
use_x86_path();
#else
use_generic_path();
#endif
#![allow(unused)]
fn main() {
// Rust — attribute-based conditional compilation
#[cfg(debug_assertions)]
fn log_verbose(msg: &str) { eprintln!("[VERBOSE] {msg}"); }
#[cfg(not(debug_assertions))]
fn log_verbose(_msg: &str) { /* compiled away in release */ }
// Combine conditions:
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn use_x86_path() { /* ... */ }
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn use_generic_path() { /* ... */ }
// Runtime check (condition is still compile-time, but usable in expressions):
if cfg!(target_os = "windows") {
println!("Running on Windows");
}
}
Cargo.toml 中的特性标志
# Cargo.toml — replace #ifdef FEATURE_FOO
[features]
default = ["json"]
json = ["dep:serde_json"] # Optional dependency
verbose-logging = [] # Flag with no extra dependency
gpu-support = ["dep:cuda-sys"] # Optional GPU support
#![allow(unused)]
fn main() {
// Conditional code based on feature flags:
#[cfg(feature = "json")]
pub fn parse_config(data: &str) -> Result<Config, Error> {
serde_json::from_str(data).map_err(Error::from)
}
#[cfg(feature = "verbose-logging")]
macro_rules! verbose {
($($arg:tt)*) => { eprintln!("[VERBOSE] {}", format!($($arg)*)); }
}
#[cfg(not(feature = "verbose-logging"))]
macro_rules! verbose {
($($arg:tt)*) => { }; // Compiles to nothing
}
}
#define MACRO(x) → macro_rules!
// C++ — textual substitution, notoriously error-prone
#define DIAG_CHECK(cond, msg) \
do { if (!(cond)) { log_error(msg); return false; } } while(0)
#![allow(unused)]
fn main() {
// Rust — hygienic, type-checked, operates on syntax tree
macro_rules! diag_check {
($cond:expr, $msg:expr) => {
if !($cond) {
log_error($msg);
return Err(DiagError::CheckFailed($msg.to_string()));
}
};
}
fn run_test() -> Result<(), DiagError> {
diag_check!(temperature < 85.0, "GPU too hot");
diag_check!(voltage > 0.8, "Rail voltage too low");
Ok(())
}
}
| C++ 预处理器 | Rust 等价 | 优势 |
|---|---|---|
#define PI 3.14 | const PI: f64 = 3.14; | 有类型、有作用域、调试器可见 |
#define MAX(a,b) ((a)>(b)?(a):(b)) | macro_rules! 或泛型 fn max<T: Ord> | 无双求值 bug |
#ifdef DEBUG | #[cfg(debug_assertions)] | 编译器检查,无拼写风险 |
#ifdef FEATURE_X | #[cfg(feature = "x")] | Cargo 管理特性;感知依赖 |
#include "header.h" | mod module; + use module::Item; | 无 include guard、无循环包含 |
#pragma once | 不需要 | 每个 .rs 文件是一个模块 — 只包含一次 |
头文件与 #include → 模块与 use
在 C++ 中,编译模型围绕文本包含:
// widget.h — every translation unit that uses Widget includes this
#pragma once
#include <string>
#include <vector>
class Widget {
public:
Widget(std::string name);
void activate();
private:
std::string name_;
std::vector<int> data_;
};
// widget.cpp — separate definition
#include "widget.h"
Widget::Widget(std::string name) : name_(std::move(name)) {}
void Widget::activate() { /* ... */ }
在 Rust 中,没有头文件、没有前向声明、没有 include guard:
#![allow(unused)]
fn main() {
// src/widget.rs — declaration AND definition in one file
pub struct Widget {
name: String, // Private by default
data: Vec<i32>,
}
impl Widget {
pub fn new(name: String) -> Self {
Widget { name, data: Vec::new() }
}
pub fn activate(&self) { /* ... */ }
}
}
// src/main.rs — import by module path
mod widget; // Tells compiler to include src/widget.rs
use widget::Widget;
fn main() {
let w = Widget::new("sensor".to_string());
w.activate();
}
| C++ | Rust | 为何更好 |
|---|---|---|
#include "foo.h" | 父模块中 mod foo; + use foo::Item; | 非文本包含,无 ODR 违反 |
#pragma once / include guard | 不需要 | 每个 .rs 文件是一个模块 — 只编译一次 |
| 前向声明 | 不需要 | 编译器看到整个 crate;顺序无关 |
class Foo;(不完整类型) | 不需要 | 无声明/定义分离 |
每个类 .h + .cpp | 单个 .rs 文件 | 无声明/定义不匹配 bug |
using namespace std; | use std::collections::HashMap; | 始终显式 — 无全局命名空间污染 |
嵌套 namespace a::b | 嵌套 mod a { mod b { } } 或 a/b.rs | 文件系统镜像模块树 |
friend 与访问控制 → 模块可见性
C++ 用 friend 授予特定类或函数访问私有成员的权限。
Rust 没有 friend 关键字 — 而是按模块划分可见性:
// C++
class Engine {
friend class Car; // Car can access private members
int rpm_;
void set_rpm(int r) { rpm_ = r; }
public:
int rpm() const { return rpm_; }
};
// Rust — items in the same module can access all fields, no `friend` needed
mod vehicle {
pub struct Engine {
rpm: u32, // Private to the module (not to the struct!)
}
impl Engine {
pub fn new() -> Self { Engine { rpm: 0 } }
pub fn rpm(&self) -> u32 { self.rpm }
}
pub struct Car {
engine: Engine,
}
impl Car {
pub fn new() -> Self { Car { engine: Engine::new() } }
pub fn accelerate(&mut self) {
self.engine.rpm = 3000; // ✅ Same module — direct field access
}
pub fn rpm(&self) -> u32 {
self.engine.rpm // ✅ Same module — can read private field
}
}
}
fn main() {
let mut car = vehicle::Car::new();
car.accelerate();
// car.engine.rpm = 9000; // ❌ Compile error: `engine` is private
println!("RPM: {}", car.rpm()); // ✅ Public method on Car
}
| C++ 访问 | Rust 等价 | 作用域 |
|---|---|---|
private | (默认,无关键字) | 仅同一模块内可访问 |
protected | 无直接等价 | 用 pub(super) 供父模块访问 |
public | pub | 到处可访问 |
friend class Foo | 将 Foo 放在同一模块 | 模块级隐私替代 friend |
| — | pub(crate) | crate 内可见,外部依赖不可见 |
| — | pub(super) | 仅父模块可见 |
| — | pub(in crate::path) | 在指定模块子树内可见 |
要点:C++ 隐私按类划分。Rust 隐私按模块划分。 这意味着通过选择哪些类型放在同一模块来控制访问 — 同处一模块的类型可完全访问彼此的私有字段。
volatile → 原子操作与 read_volatile/write_volatile
在 C++ 中,volatile 告诉编译器不要优化掉读写 — 通常用于内存映射硬件寄存器。Rust 没有 volatile 关键字。
// C++: volatile for hardware registers
volatile uint32_t* const GPIO_REG = reinterpret_cast<volatile uint32_t*>(0x4002'0000);
*GPIO_REG = 0x01; // Write not optimized away
uint32_t val = *GPIO_REG; // Read not optimized away
#![allow(unused)]
fn main() {
// Rust: explicit volatile operations — only in unsafe code
use std::ptr;
const GPIO_REG: *mut u32 = 0x4002_0000 as *mut u32;
// SAFETY: GPIO_REG is a valid memory-mapped I/O address.
unsafe {
ptr::write_volatile(GPIO_REG, 0x01); // Write not optimized away
let val = ptr::read_volatile(GPIO_REG); // Read not optimized away
}
}
对于并发共享状态(C++ volatile 的另一常见误用),Rust 使用原子类型:
// C++: volatile is NOT sufficient for thread safety (common mistake!)
volatile bool stop_flag = false; // ❌ Data race — UB in C++11+
// Correct C++:
std::atomic<bool> stop_flag{false};
#![allow(unused)]
fn main() {
// Rust: atomics are the only way to share mutable state across threads
use std::sync::atomic::{AtomicBool, Ordering};
static STOP_FLAG: AtomicBool = AtomicBool::new(false);
// From another thread:
STOP_FLAG.store(true, Ordering::Release);
// Check:
if STOP_FLAG.load(Ordering::Acquire) {
println!("Stopping");
}
}
| C++ 用法 | Rust 等价 | 说明 |
|---|---|---|
硬件寄存器用 volatile | ptr::read_volatile / ptr::write_volatile | 需要 unsafe — 适用于 MMIO |
线程信号用 volatile | AtomicBool / AtomicU32 等 | C++ 用 volatile 做这也错! |
std::atomic<T> | std::sync::atomic::AtomicT | 语义相同,顺序相同 |
std::atomic<T>::load(memory_order_acquire) | AtomicT::load(Ordering::Acquire) | 1:1 映射 |
static 变量 → static、const、LazyLock、OnceLock
基本 static 与 const
// C++
const int MAX_RETRIES = 5; // Compile-time constant
static std::string CONFIG_PATH = "/etc/app"; // Static init — order undefined!
#![allow(unused)]
fn main() {
// Rust
const MAX_RETRIES: u32 = 5; // Compile-time constant, inlined
static CONFIG_PATH: &str = "/etc/app"; // 'static lifetime, fixed address
}
静态初始化顺序灾难
C++ 有一个著名问题:不同翻译单元中的全局构造函数以未指定顺序
执行。Rust 完全避免这一点 — static 值必须是编译期常量(无构造函数)。
运行时初始化的全局变量用 LazyLock(Rust 1.80+)或 OnceLock:
#![allow(unused)]
fn main() {
use std::sync::LazyLock;
// Equivalent to C++ `static std::regex` — initialized on first access, thread-safe
static CONFIG_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"^[a-z]+_diag$").expect("invalid regex")
});
fn is_valid_diag(name: &str) -> bool {
CONFIG_REGEX.is_match(name) // First call initializes; subsequent calls are fast
}
}
#![allow(unused)]
fn main() {
use std::sync::OnceLock;
// OnceLock: initialized once, can be set from runtime data
static DB_CONN: OnceLock<String> = OnceLock::new();
fn init_db(connection_string: &str) {
DB_CONN.set(connection_string.to_string())
.expect("DB_CONN already initialized");
}
fn get_db() -> &'static str {
DB_CONN.get().expect("DB not initialized")
}
}
| C++ | Rust | 说明 |
|---|---|---|
const int X = 5; | const X: i32 = 5; | 均为编译期。Rust 需要类型注解 |
constexpr int X = 5; | const X: i32 = 5; | Rust const 始终是 constexpr |
static int count = 0;(文件作用域) | static COUNT: AtomicI32 = AtomicI32::new(0); | 可变 static 需要 unsafe 或原子类型 |
static std::string s = "hi"; | static S: &str = "hi"; 或 LazyLock<String> | 简单情况无运行时构造函数 |
static MyObj obj;(复杂初始化) | static OBJ: LazyLock<MyObj> = LazyLock::new(|| { ... }); | 线程安全、惰性、无初始化顺序问题 |
thread_local | thread_local! { static X: Cell<u32> = Cell::new(0); } | 语义相同 |
constexpr → const fn
C++ constexpr 标记函数与变量供编译期求值。Rust
用 const fn 与 const 达到同样目的:
// C++
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int val = factorial(5); // Computed at compile time → 120
#![allow(unused)]
fn main() {
// Rust
const fn factorial(n: u32) -> u32 {
if n <= 1 { 1 } else { n * factorial(n - 1) }
}
const VAL: u32 = factorial(5); // Computed at compile time → 120
// Also works in array sizes and match patterns:
const LOOKUP: [u32; 5] = [factorial(1), factorial(2), factorial(3),
factorial(4), factorial(5)];
}
| C++ | Rust | 说明 |
|---|---|---|
constexpr int f() | const fn f() -> i32 | 同样意图 — 可编译期求值 |
constexpr 变量 | const 变量 | Rust const 始终是编译期 |
consteval(C++20) | 无等价物 | const fn 也可在运行时运行 |
if constexpr(C++17) | 无等价物(用 cfg! 或泛型) | trait 特化可覆盖部分用例 |
constinit(C++20) | 带 const 初始化的 static | Rust static 默认必须 const 初始化 |
const fn的当前限制(截至 Rust 1.82 已稳定):
- 无 trait 方法(const 上下文中不能对
Vec调用.len())- 无堆分配(
Box::new、Vec::new不能 const)无浮点运算— Rust 1.82 已稳定- 不能用
for循环(用递归或带手动索引的while)
SFINAE 与 enable_if → Trait 约束与 where 子句
在 C++ 中,SFINAE(替换失败不是错误)是条件泛型编程背后的机制。它强大但出了名的难读。Rust 完全用 trait 约束替代:
// C++: SFINAE-based conditional function (pre-C++20)
template<typename T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
T double_it(T val) { return val * 2; }
template<typename T,
std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T double_it(T val) { return val * 2.0; }
// C++20 concepts — cleaner but still verbose:
template<std::integral T>
T double_it(T val) { return val * 2; }
#![allow(unused)]
fn main() {
// Rust: trait bounds — readable, composable, excellent error messages
use std::ops::Mul;
fn double_it<T: Mul<Output = T> + From<u8>>(val: T) -> T {
val * T::from(2)
}
// Or with where clause for complex bounds:
fn process<T>(val: T) -> String
where
T: std::fmt::Display + Clone + Send,
{
format!("Processing: {}", val)
}
// Conditional behavior via separate impls (replaces SFINAE overloads):
trait Describable {
fn describe(&self) -> String;
}
impl Describable for u32 {
fn describe(&self) -> String { format!("integer: {self}") }
}
impl Describable for f64 {
fn describe(&self) -> String { format!("float: {self:.2}") }
}
}
| C++ 模板元编程 | Rust 等价 | 可读性 |
|---|---|---|
std::enable_if_t<cond> | where T: Trait | 🟢 清晰自然语言 |
std::is_integral_v<T> | 数值 trait 或具体类型的约束 | 🟢 无 _v / _t 后缀 |
| SFINAE 重载集 | 分离的 impl Trait for ConcreteType 块 | 🟢 每个 impl 独立 |
if constexpr (std::is_same_v<T, int>) | 通过 trait impl 特化 | 🟢 编译期分发 |
C++20 concept | trait | 🟢 意图几乎相同 |
requires 子句 | where 子句 | 🟢 位置与语法相似 |
| 模板深处编译失败 | 调用点 trait 不匹配即失败 | 🟢 无 200 行错误级联 |
要点:C++ concepts(C++20)最接近 Rust trait。 若熟悉 C++20 concepts,可把 Rust trait 视为自 1.0 起就是一等语言特性的 concepts, 有一致的实现模型(trait impl)而非鸭子类型。
std::function → 函数指针、impl Fn 与 Box<dyn Fn>
C++ std::function<R(Args...)> 是类型擦除的可调用对象。Rust 有三种选择,
各有权衡:
// C++: one-size-fits-all (heap-allocated, type-erased)
#include <functional>
std::function<int(int)> make_adder(int n) {
return [n](int x) { return x + n; };
}
#![allow(unused)]
fn main() {
// Rust Option 1: fn pointer — simple, no captures, no allocation
fn add_one(x: i32) -> i32 { x + 1 }
let f: fn(i32) -> i32 = add_one;
println!("{}", f(5)); // 6
// Rust Option 2: impl Fn — monomorphized, zero overhead, can capture
fn apply(val: i32, f: impl Fn(i32) -> i32) -> i32 { f(val) }
let n = 10;
let result = apply(5, |x| x + n); // Closure captures `n`
// Rust Option 3: Box<dyn Fn> — type-erased, heap-allocated (like std::function)
fn make_adder(n: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + n)
}
let adder = make_adder(10);
println!("{}", adder(5)); // 15
// Storing heterogeneous callables (like vector<function<int(int)>>):
let callbacks: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|x| x + 1),
Box::new(|x| x * 2),
Box::new(make_adder(100)),
];
for cb in &callbacks {
println!("{}", cb(5)); // 6, 10, 105
}
}
| 何时使用 | C++ 等价 | Rust 选择 |
|---|---|---|
| 顶层函数,无捕获 | 函数指针 | fn(Args) -> Ret |
| 泛型函数接受可调用对象 | 模板参数 | impl Fn(Args) -> Ret(静态分发) |
| 泛型中的 trait 约束 | template<typename F> | F: Fn(Args) -> Ret |
| 存储可调用对象,类型擦除 | std::function<R(Args)> | Box<dyn Fn(Args) -> Ret> |
| 修改状态的可回调 | 可变 lambda 的 std::function | Box<dyn FnMut(Args) -> Ret> |
| 一次性回调(被消费) | 移动的 std::function | Box<dyn FnOnce(Args) -> Ret> |
性能说明:
impl Fn零开销(单态化,类似 C++ 模板)。Box<dyn Fn>与std::function开销相同(vtable + 堆分配)。 除非需要存储异构可调用对象,否则优先impl Fn。
容器映射:C++ STL → Rust std::collections
| C++ STL 容器 | Rust 等价 | 说明 |
|---|---|---|
std::vector<T> | Vec<T> | API 几乎相同。Rust 默认做边界检查 |
std::array<T, N> | [T; N] | 栈上固定大小数组 |
std::deque<T> | std::collections::VecDeque<T> | 环形缓冲区。两端 push/pop 高效 |
std::list<T> | std::collections::LinkedList<T> | Rust 中很少用 — Vec 几乎总是更快 |
std::forward_list<T> | 无等价物 | 用 Vec 或 VecDeque |
std::unordered_map<K, V> | std::collections::HashMap<K, V> | 默认 SipHash(抗 DoS) |
std::map<K, V> | std::collections::BTreeMap<K, V> | B 树;键有序;要求 K: Ord |
std::unordered_set<T> | std::collections::HashSet<T> | 要求 T: Hash + Eq |
std::set<T> | std::collections::BTreeSet<T> | 有序集合;要求 T: Ord |
std::priority_queue<T> | std::collections::BinaryHeap<T> | 默认最大堆(与 C++ 相同) |
std::stack<T> | 带 .push() / .pop() 的 Vec<T> | 无需单独 stack 类型 |
std::queue<T> | 带 .push_back() / .pop_front() 的 VecDeque<T> | 无需单独 queue 类型 |
std::string | String | 保证 UTF-8,非 null 终止 |
std::string_view | &str | 借用的 UTF-8 切片 |
std::span<T>(C++20) | &[T] / &mut [T] | Rust 切片自 1.0 即为一等类型 |
std::tuple<A, B, C> | (A, B, C) | 一等语法,可解构 |
std::pair<A, B> | (A, B) | 就是二元组 |
std::bitset<N> | 标准库无等价 | 用 bitvec crate 或 [u8; N/8] |
主要差异:
- Rust 的
HashMap/HashSet要求K: Hash + Eq— 编译器在类型层面强制,不像 C++ 用不可哈希键会在 STL 深处报模板错误 Vec索引(v[i])默认越界会 panic。用.get(i)得到Option<&T>,或迭代器完全避免边界检查- 无
std::multimap或std::multiset— 用HashMap<K, Vec<V>>或BTreeMap<K, Vec<V>>
异常安全 → Panic 安全
C++ 定义三级异常安全(Abrahams 保证):
| C++ 级别 | 含义 | Rust 等价 |
|---|---|---|
| No-throw | 函数从不抛出 | 函数从不 panic(返回 Result) |
| Strong(提交或回滚) | 若抛出,状态不变 | 所有权模型使这很自然 — ? 早退时,部分构建的值会被 drop |
| Basic | 若抛出,不变量保持 | Rust 默认 — Drop 运行,无泄漏 |
Rust 所有权模型如何帮助
#![allow(unused)]
fn main() {
// Strong guarantee for free — if file.write() fails, config is unchanged
fn update_config(config: &mut Config, path: &str) -> Result<(), Error> {
let new_data = fetch_from_network()?; // Err → early return, config untouched
let validated = validate(new_data)?; // Err → early return, config untouched
*config = validated; // Only reached on success (commit)
Ok(())
}
}
在 C++ 中,强保证需要手动回滚或 copy-and-swap
惯用法。在 Rust 中,? 传播对大多数代码默认给出强保证。
catch_unwind — Rust 的 catch(...) 等价物
#![allow(unused)]
fn main() {
use std::panic;
// Catch a panic (like catch(...) in C++) — rarely needed
let result = panic::catch_unwind(|| {
// Code that might panic
let v = vec![1, 2, 3];
v[10] // Panics! (index out of bounds)
});
match result {
Ok(val) => println!("Got: {val}"),
Err(_) => eprintln!("Caught a panic — cleaned up"),
}
}
UnwindSafe — 标记 panic 安全类型
#![allow(unused)]
fn main() {
use std::panic::UnwindSafe;
// Types behind &mut are NOT UnwindSafe by default — the panic may have
// left them in a partially-modified state
fn safe_execute<F: FnOnce() + UnwindSafe>(f: F) {
let _ = std::panic::catch_unwind(f);
}
// Use AssertUnwindSafe to override when you've audited the code:
use std::panic::AssertUnwindSafe;
let mut data = vec![1, 2, 3];
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
data.push(4);
}));
}
| C++ 异常模式 | Rust 等价 |
|---|---|
throw MyException() | return Err(MyError::...)(首选)或 panic!("...") |
try { } catch (const E& e) | match result { Ok(v) => ..., Err(e) => ... } 或 ? |
catch (...) | std::panic::catch_unwind(...) |
noexcept | -> Result<T, E>(错误是值,不是异常) |
| 栈展开中的 RAII 清理 | panic 展开时运行 Drop::drop() |
std::uncaught_exceptions() | std::thread::panicking() |
-fno-exceptions 编译选项 | Cargo.toml [profile] 中 panic = "abort" |
结论:Rust 中大多数代码用
Result<T, E>而非异常, 使错误路径显式且可组合。panic!留给 bug (如assert!失败),不是常规错误。这意味着「异常安全」 基本上不是问题 — 所有权系统自动处理清理。
C++ 到 Rust 迁移模式
速查:C++ → Rust 惯用法映射
| C++ 模式 | Rust 惯用法 | 说明 |
|---|---|---|
class Derived : public Base | enum Variant { A {...}, B {...} } | 封闭集合优先用枚举 |
virtual void method() = 0 | trait MyTrait { fn method(&self); } | 用于开放/可扩展接口 |
dynamic_cast<Derived*>(ptr) | match value { Variant::A(data) => ..., } | 穷尽,无运行时失败 |
vector<unique_ptr<Base>> | Vec<Box<dyn Trait>> | 仅当真正需要多态时 |
shared_ptr<T> | Rc<T> 或 Arc<T> | 优先 Box<T> 或拥有值 |
enable_shared_from_this<T> | 竞技场模式(Vec<T> + 索引) | 彻底消除引用环 |
每个类中的 Base* m_pFramework | fn execute(&mut self, ctx: &mut Context) | 传上下文,不要存指针 |
try { } catch (...) { } | match result { Ok(v) => ..., Err(e) => ... } | 或用 ? 传播 |
std::optional<T> | Option<T> | 必须 match,不能忘记 None |
const std::string& 参数 | &str 参数 | 同时接受 String 与 &str |
enum class Foo { A, B, C } | enum Foo { A, B, C } | Rust 枚举也可携带数据 |
auto x = std::move(obj) | let x = obj; | 移动是默认,无需 std::move |
| CMake + make + lint | cargo build / test / clippy / fmt | 一个工具搞定一切 |
迁移策略
- 从数据类型开始:先翻译结构体与枚举 — 迫使你思考所有权
- 工厂转枚举:若工厂创建不同派生类型,多半应是
enum+match - 上帝对象拆成组合结构体:将相关字段分组到专注的结构体
- 指针换借用:将存储的
Base*转为带生命周期的&'a T借用 - 谨慎使用
Box<dyn Trait>:仅用于插件系统与测试 mock - 让编译器引导你:Rust 错误信息很好 — 仔细阅读
19. Rust 宏:从预处理器到元编程
Rust 宏:从预处理器到元编程
你将学到: Rust 宏如何工作、何时用宏而非函数或泛型,以及它们如何替代 C/C++ 预处理器。读完本章你能编写自己的
macro_rules!宏,并理解#[derive(Debug)]底层做了什么。
宏是你在 Rust 中最早遇到的东西之一(第一行就是 println!("hello")),却是多数课程最后才讲的内容。本章补上这一课。
为何需要宏
函数与泛型处理 Rust 中大部分代码复用。宏填补类型系统够不着的地方:
| 需求 | 函数/泛型? | 宏? | 原因 |
|---|---|---|---|
| 计算一个值 | ✅ fn max<T: Ord>(a: T, b: T) -> T | — | 类型系统能处理 |
| 接受可变数量参数 | ❌ Rust 无可变参数函数 | ✅ println!("{} {}", a, b) | 宏可接受任意数量 token |
生成重复的 impl 块 | ❌ 单靠泛型做不到 | ✅ macro_rules! | 宏在编译期生成代码 |
| 编译期运行代码 | ❌ const fn 有限制 | ✅ 过程宏 | 编译期可运行完整 Rust 代码 |
| 条件包含代码 | ❌ | ✅ #[cfg(...)] | 属性宏控制编译 |
若你来自 C/C++,可把宏视为预处理器唯一正确的替代 — 区别在于它们操作语法树而非原始文本,因此是卫生的(不会意外名称冲突)且类型感知。
面向 C 开发者: Rust 宏完全替代
#define。没有文本预处理器。完整预处理器 → Rust 映射见 ch18。
用 macro_rules! 声明宏
声明宏(也称「按示例宏」)是 Rust 最常见的宏形式。它们对语法做模式匹配,类似对值做 match。
基本语法
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
fn main() {
say_hello!(); // Expands to: println!("Hello!");
}
名称后的 ! 告诉你(和编译器)这是宏调用。
带参数的模式匹配
宏用片段说明符对 token 树 做匹配:
macro_rules! greet {
// Pattern 1: no arguments
() => {
println!("Hello, world!");
};
// Pattern 2: one expression argument
($name:expr) => {
println!("Hello, {}!", $name);
};
}
fn main() {
greet!(); // "Hello, world!"
greet!("Rust"); // "Hello, Rust!"
}
片段说明符速查
| 说明符 | 匹配 | 示例 |
|---|---|---|
$x:expr | 任意表达式 | 42, a + b, foo() |
$x:ty | 类型 | i32, Vec<String>, &str |
$x:ident | 标识符 | foo, my_var |
$x:pat | 模式 | Some(x), _, (a, b) |
$x:stmt | 语句 | let x = 5; |
$x:block | 块 | { println!("hi"); 42 } |
$x:literal | 字面量 | 42, "hello", true |
$x:tt | 单个 token 树 | 任意 — 通配符 |
$x:item | 项(fn、struct、impl 等) | fn foo() {} |
重复 — 杀手级特性
C/C++ 宏不能循环。Rust 宏可以重复模式:
macro_rules! make_vec {
// Match zero or more comma-separated expressions
( $( $element:expr ),* ) => {
{
let mut v = Vec::new();
$( v.push($element); )* // Repeat for each matched element
v
}
};
}
fn main() {
let v = make_vec![1, 2, 3, 4, 5];
println!("{v:?}"); // [1, 2, 3, 4, 5]
}
$( ... ),* 语法表示「匹配零个或多个该模式,逗号分隔」。展开中的 $( ... )* 对每个匹配重复一次体。
标准库里的
vec![]就是这样实现的。 实际源码为:#![allow(unused)] fn main() { macro_rules! vec { () => { Vec::new() }; ($elem:expr; $n:expr) => { vec::from_elem($elem, $n) }; ($($x:expr),+ $(,)?) => { <[_]>::into_vec(Box::new([$($x),+])) }; } }末尾的
$(,)?允许可选的尾逗号。
重复运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
$( ... )* | 零个或多个 | vec![], vec![1], vec![1, 2, 3] |
$( ... )+ | 一个或多个 | 至少需要一个元素 |
$( ... )? | 零个或一个 | 可选元素 |
实用示例:hashmap! 构造器
标准库有 vec![] 但没有 hashmap!{}。我们来写一个:
macro_rules! hashmap {
( $( $key:expr => $value:expr ),* $(,)? ) => {
{
let mut map = std::collections::HashMap::new();
$( map.insert($key, $value); )*
map
}
};
}
fn main() {
let scores = hashmap! {
"Alice" => 95,
"Bob" => 87,
"Carol" => 92, // trailing comma OK thanks to $(,)?
};
println!("{scores:?}");
}
实用示例:诊断检查宏
嵌入式/诊断代码中的常见模式 — 检查条件并返回错误:
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Error, Debug)]
enum DiagError {
#[error("Check failed: {0}")]
CheckFailed(String),
}
macro_rules! diag_check {
($cond:expr, $msg:expr) => {
if !($cond) {
return Err(DiagError::CheckFailed($msg.to_string()));
}
};
}
fn run_diagnostics(temp: f64, voltage: f64) -> Result<(), DiagError> {
diag_check!(temp < 85.0, "GPU too hot");
diag_check!(voltage > 0.8, "Rail voltage too low");
diag_check!(voltage < 1.5, "Rail voltage too high");
println!("All checks passed");
Ok(())
}
}
C/C++ 对比:
// C preprocessor — textual substitution, no type safety, no hygiene #define DIAG_CHECK(cond, msg) \ do { if (!(cond)) { log_error(msg); return -1; } } while(0)Rust 版本返回正确的
Result类型,无双求值风险,编译器会检查$cond是否为bool表达式。
卫生性:为何 Rust 宏安全
C/C++ 宏 bug 常来自名称冲突:
// C: dangerous — `x` could shadow the caller's `x`
#define SQUARE(x) ((x) * (x))
int x = 5;
int result = SQUARE(x++); // UB: x incremented twice!
Rust 宏是卫生的 — 宏内创建的变量不会泄漏到外面:
macro_rules! make_x {
() => {
let x = 42; // This `x` is scoped to the macro expansion
};
}
fn main() {
let x = 10;
make_x!();
println!("{x}"); // Prints 10, not 42 — hygiene prevents collision
}
宏的 x 与调用者的 x 被编译器视为不同变量,尽管同名。C 预处理器做不到这一点。
常见标准库宏
你从第 1 章就在用这些 — 它们实际做什么:
| 宏 | 作用 | 展开为(简化) |
|---|---|---|
println!("{}", x) | 格式化打印到 stdout 并换行 | std::io::_print(format_args!(...)) |
eprintln!("{}", x) | 打印到 stderr 并换行 | 同上但到 stderr |
format!("{}", x) | 格式化为 String | 分配并返回 String |
vec![1, 2, 3] | 用元素创建 Vec | Vec::from([1, 2, 3])(近似) |
todo!() | 标记未完成代码 | panic!("not yet implemented") |
unimplemented!() | 标记故意未实现代码 | panic!("not implemented") |
unreachable!() | 标记编译器无法证明不可达的代码 | panic!("unreachable") |
assert!(cond) | 条件为假则 panic | if !cond { panic!(...) } |
assert_eq!(a, b) | 值不等则 panic | 失败时显示两个值 |
dbg!(expr) | 将表达式与值打印到 stderr 并返回值 | eprintln!("[file:line] expr = {:#?}", &expr); expr |
include_str!("file.txt") | 编译期将文件内容嵌入为 &str | 编译时读取文件 |
include_bytes!("data.bin") | 编译期将文件内容嵌入为 &[u8] | 编译时读取文件 |
cfg!(condition) | 编译期条件作为 bool | 根据目标为 true 或 false |
env!("VAR") | 编译期读取环境变量 | 未设置则编译失败 |
concat!("a", "b") | 编译期拼接字面量 | "ab" |
dbg! — 日常调试宏
fn factorial(n: u32) -> u32 {
if dbg!(n <= 1) { // Prints: [src/main.rs:2] n <= 1 = false
dbg!(1) // Prints: [src/main.rs:3] 1 = 1
} else {
dbg!(n * factorial(n - 1)) // Prints intermediate values
}
}
fn main() {
dbg!(factorial(4)); // Prints all recursive calls with file:line
}
dbg! 返回它包裹的值,因此可插入任意位置而不改变程序行为。它打印到 stderr(非 stdout),不干扰程序输出。提交代码前删除所有 dbg! 调用。
格式字符串语法
由于 println!、format!、eprintln! 和 write! 共用同一套格式机制,速查如下:
#![allow(unused)]
fn main() {
let name = "sensor";
let value = 3.14159;
let count = 42;
println!("{name}"); // Variable by name (Rust 1.58+)
println!("{}", name); // Positional
println!("{value:.2}"); // 2 decimal places: "3.14"
println!("{count:>10}"); // Right-aligned, width 10: " 42"
println!("{count:0>10}"); // Zero-padded: "0000000042"
println!("{count:#06x}"); // Hex with prefix: "0x002a"
println!("{count:#010b}"); // Binary with prefix: "0b00101010"
println!("{value:?}"); // Debug format
println!("{value:#?}"); // Pretty-printed Debug format
}
面向 C 开发者: 可把它当作类型安全的
printf— 编译器检查{:.2}是否用于浮点数而非字符串。没有%s/%d格式不匹配 bug。面向 C++ 开发者: 这用一条可读格式字符串替代
std::cout << std::fixed << std::setprecision(2) << value。
Derive 宏
你在本书几乎每个结构体上都见过 #[derive(...)]:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
}
#[derive(Debug)] 是一种 derive 宏 — 自动生成 trait 实现的过程宏。它生成的大致如下(简化):
#![allow(unused)]
fn main() {
// What #[derive(Debug)] generates for Point:
impl std::fmt::Debug for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
}
没有 #[derive(Debug)],你得为每个结构体手写该 impl 块。
常用 derive trait
| Derive | 生成内容 | 何时使用 |
|---|---|---|
Debug | {:?} 格式化 | 几乎总是 — 便于调试打印 |
Clone | .clone() 方法 | 需要复制值时 |
Copy | 赋值时隐式复制 | 小的、仅栈上的类型(整数、[f64; 3]) |
PartialEq / Eq | == 与 != 运算符 | 需要相等比较时 |
PartialOrd / Ord | <、>、<=、>= 运算符 | 需要排序时 |
Hash | 用于 HashMap/HashSet 键的哈希 | 用作 map 键的类型 |
Default | Type::default() 构造器 | 有合理零/空值的类型 |
serde::Serialize / Deserialize | JSON/TOML 等序列化 | 跨 API 边界的数据类型 |
derive 决策树
Should I derive it?
│
├── Does my type contain only types that implement the trait?
│ ├── Yes → #[derive] will work
│ └── No → Write a manual impl (or skip it)
│
└── Will users of my type reasonably expect this behavior?
├── Yes → Derive it (Debug, Clone, PartialEq are almost always reasonable)
└── No → Don't derive (e.g., don't derive Copy for a type with a file handle)
C++ 对比:
#[derive(Clone)]类似自动生成正确的拷贝构造函数。#[derive(PartialEq)]类似自动生成逐字段比较的operator==— C++20 的= default三路比较运算符终于提供了类似能力。
属性宏
属性宏变换所附的项。你已经用过几种:
#![allow(unused)]
fn main() {
#[test] // Marks a function as a test
fn test_addition() {
assert_eq!(2 + 2, 4);
}
#[cfg(target_os = "linux")] // Conditionally includes this function
fn linux_only() { /* ... */ }
#[derive(Debug)] // Generates Debug implementation
struct MyType { /* ... */ }
#[allow(dead_code)] // Suppresses a compiler warning
fn unused_helper() { /* ... */ }
#[must_use] // Warn if return value is discarded
fn compute_checksum(data: &[u8]) -> u32 { /* ... */ }
}
常见内置属性:
| 属性 | 用途 |
|---|---|
#[test] | 标记为测试函数 |
#[cfg(...)] | 条件编译 |
#[derive(...)] | 自动生成 trait impl |
#[allow(...)] / #[deny(...)] / #[warn(...)] | 控制 lint 级别 |
#[must_use] | 未使用返回值时警告 |
#[inline] / #[inline(always)] | 提示内联函数 |
#[repr(C)] | 使用 C 兼容内存布局(用于 FFI) |
#[no_mangle] | 不修饰符号名(用于 FFI) |
#[deprecated] | 标记为已弃用,可选消息 |
面向 C/C++ 开发者: 属性替代了预处理器指令(
#pragma、__attribute__((...)))和编译器扩展的混合体。它们是语言语法的一部分,不是后装扩展。
过程宏(概念概览)
过程宏(「proc macro」)是作为独立 Rust 程序在编译期运行并生成代码的宏。比 macro_rules! 更强大也更复杂。
有三种:
| 种类 | 语法 | 示例 | 作用 |
|---|---|---|---|
| 函数式 | my_macro!(...) | sql!(SELECT * FROM users) | 解析自定义语法,生成 Rust 代码 |
| Derive | #[derive(MyTrait)] | #[derive(Serialize)] | 从结构体定义生成 trait impl |
| 属性 | #[my_attr] | #[tokio::main]、#[instrument] | 变换被注解的项 |
你已经用过过程宏
thiserror的#[derive(Error)]— 为错误枚举生成Display与Fromimplserde的#[derive(Serialize, Deserialize)]— 生成序列化代码#[tokio::main]— 将async fn main()变换为运行时设置 + block_on#[test]— 由测试框架注册(内置过程宏)
何时编写自己的过程宏
本课程中你可能不需要写过程宏。它们在以下情况有用:
- 需要在编译期检查结构体字段/枚举变体(derive 宏)
- 构建领域特定语言(函数式宏)
- 需要变换函数签名(属性宏)
对大多数代码,macro_rules! 或普通函数就够。
C++ 对比: 过程宏承担 C++ 中代码生成器、模板元编程以及
protoc等外部工具的角色。区别在于过程宏是 cargo 构建流水线的一部分 — 无外部构建步骤,无 CMake 自定义命令。
何时用何物:宏 vs 函数 vs 泛型
Need to generate code?
│
├── No → Use a function or generic function
│ (simpler, better error messages, IDE support)
│
└── Yes ─┬── Variable number of arguments?
│ └── Yes → macro_rules! (e.g., println!, vec!)
│
├── Repetitive impl blocks for many types?
│ └── Yes → macro_rules! with repetition
│
├── Need to inspect struct fields?
│ └── Yes → Derive macro (proc macro)
│
├── Need custom syntax (DSL)?
│ └── Yes → Function-like proc macro
│
└── Need to transform a function/struct?
└── Yes → Attribute proc macro
一般准则: 若函数或泛型能做到,就不要用宏。宏的错误信息更差、宏体内无 IDE 自动补全,也更难调试。
练习
🟢 练习 1:min! 宏
编写 min! 宏:
min!(a, b)返回两个值中较小者min!(a, b, c)返回三个值中最小者- 适用于任何实现
PartialOrd的类型
提示: 你的 macro_rules! 需要两个 match 分支。
解答(点击展开)
macro_rules! min {
($a:expr, $b:expr) => {
if $a < $b { $a } else { $b }
};
($a:expr, $b:expr, $c:expr) => {
min!(min!($a, $b), $c)
};
}
fn main() {
println!("{}", min!(3, 7)); // 3
println!("{}", min!(9, 2, 5)); // 2
println!("{}", min!(1.5, 0.3)); // 0.3
}
说明: 生产代码优先用 std::cmp::min 或 a.min(b)。本练习演示多分支宏的机制。
🟡 练习 2:从零实现 hashmap!
不参考上文示例,编写 hashmap! 宏:
- 从
key => value对创建HashMap - 支持尾逗号
- 适用于任何可哈希键类型
测试:
#![allow(unused)]
fn main() {
let m = hashmap! {
"name" => "Alice",
"role" => "Engineer",
};
assert_eq!(m["name"], "Alice");
assert_eq!(m.len(), 2);
}
解答(点击展开)
use std::collections::HashMap;
macro_rules! hashmap {
( $( $key:expr => $val:expr ),* $(,)? ) => {{
let mut map = HashMap::new();
$( map.insert($key, $val); )*
map
}};
}
fn main() {
let m = hashmap! {
"name" => "Alice",
"role" => "Engineer",
};
assert_eq!(m["name"], "Alice");
assert_eq!(m.len(), 2);
println!("Tests passed!");
}
🟡 练习 3:浮点比较的 assert_approx_eq!
编写宏 assert_approx_eq!(a, b, epsilon),当 |a - b| > epsilon 时 panic。用于精确相等会失败的浮点计算测试。
测试:
#![allow(unused)]
fn main() {
assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10); // Should pass
assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4); // Should pass
// assert_approx_eq!(1.0, 2.0, 0.5); // Should panic
}
解答(点击展开)
macro_rules! assert_approx_eq {
($a:expr, $b:expr, $eps:expr) => {
let (a, b, eps) = ($a as f64, $b as f64, $eps as f64);
let diff = (a - b).abs();
if diff > eps {
panic!(
"assertion failed: |{} - {}| = {} > {} (epsilon)",
a, b, diff, eps
);
}
};
}
fn main() {
assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10);
assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4);
println!("All float comparisons passed!");
}
🔴 练习 4:impl_display_for_enum!
编写宏,为简单 C 风格枚举生成 Display 实现。给定:
#![allow(unused)]
fn main() {
impl_display_for_enum! {
enum Color {
Red => "red",
Green => "green",
Blue => "blue",
}
}
}
应同时生成 enum Color { Red, Green, Blue } 定义,以及将每个变体映射到字符串的 impl Display for Color。
提示: 需要 $( ... ),* 重复和多种片段说明符。
解答(点击展开)
use std::fmt;
macro_rules! impl_display_for_enum {
(enum $name:ident { $( $variant:ident => $display:expr ),* $(,)? }) => {
#[derive(Debug, Clone, Copy, PartialEq)]
enum $name {
$( $variant ),*
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
$( $name::$variant => write!(f, "{}", $display), )*
}
}
}
};
}
impl_display_for_enum! {
enum Color {
Red => "red",
Green => "green",
Blue => "blue",
}
}
fn main() {
let c = Color::Green;
println!("Color: {c}"); // "Color: green"
println!("Debug: {c:?}"); // "Debug: Green"
assert_eq!(format!("{}", Color::Red), "red");
println!("All tests passed!");
}