Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

面向 C/C++ 程序员的 Rust 入门课程

版权与署名说明(非官方译本) 本文档为 microsoft/RustTrainingc-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 翻译模式
  • 本课程不涵盖 async Rust——请参阅配套 Async Rust Training,了解 future、执行器、Pin、tokio 及生产级异步模式

自学指南

本材料既可作为讲师授课课程,也适合自学。如果你独自学习,以下建议可帮助你充分利用:

进度建议:

章节主题建议用时检查点
1–4环境搭建、类型、控制流1 天能编写 CLI 温度转换器
5–7数据结构、所有权1–2 天能解释 为什么 let s2 = s1 会使 s1 失效
8–9模块、错误处理1 天能创建多文件项目并用 ? 传播错误
10–12Trait、泛型、闭包1–2 天能编写带 Trait 约束的泛型函数
13–14并发、unsafe/FFI1 天能用 Arc<Mutex<T>> 编写线程安全计数器
15–16深入专题自定进度参考资料——在需要时阅读
17–19最佳实践与参考自定进度编写真实代码时查阅

如何使用练习:

  • 每章都有动手练习,难度标注为:🟢 入门、🟡 中级、🔴 挑战
  • 务必在展开答案前先尝试练习。 与借用检查器较劲是学习的一部分——编译器的错误信息就是你的老师
  • 如果卡住超过 15 分钟,展开答案、研读后关闭,再从头试一次
  • Rust Playground 无需本地安装即可运行代码

遇到瓶颈时:

  • 仔细阅读编译器错误信息——Rust 的错误提示非常出色
  • 重读相关章节;所有权(第 7 章)等概念往往在第二遍才豁然开朗
  • Rust 标准库文档 质量很高——可搜索任意类型或方法
  • 异步模式请参阅配套 Async Rust Training

目录

第一部分 — 基础

1. 引言与动机

2. 快速上手

3. 基本类型与变量

4. 控制流

5. 数据结构与集合

6. 模式匹配与枚举

7. 所有权与内存管理

8. 模块与 Crate

9. 错误处理

10. Trait 与泛型

11. 类型系统高级特性

12. 函数式编程

13. 并发

14. Unsafe Rust 与 FFI

第二部分 — 深入专题

15. no_std — 裸机 Rust

16. 案例研究:真实世界的 C++ 到 Rust 翻译

第三部分 — 最佳实践与参考

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_ptrunique_ptr、RAII 和移动语义是正确方向,但只是创可贴,不是根治——移动后使用、引用循环、迭代器失效和异常安全漏洞依然敞开
  • Rust 提供你依赖的 C/C++ 级性能,同时带来编译期安全保证

📖 深入阅读: 请参阅 为什么 C/C++ 开发者需要 Rust,了解具体漏洞示例、Rust 消除问题的完整清单,以及 C++ 智能指针为何不够


Rust 如何解决这些问题?

缓冲区溢出与边界违规

  • 所有 Rust 数组、切片和字符串都关联显式边界。编译器插入检查,确保任何边界违规导致运行时崩溃(Rust 术语中为 panic)——绝不会是未定义行为

悬垂指针与引用

  • Rust 引入生命周期和借用检查,在编译期消除悬垂引用
  • 没有悬垂指针,没有释放后使用——编译器不会让你写出这类代码

移动后使用

  • Rust 的所有权系统使移动具有破坏性——一旦移动值,编译器拒绝让你再使用原值。没有僵尸对象,没有“有效但未指定状态“

资源管理

  • Rust 的 Drop Trait 是正确实现的 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();
}

数据竞争

  • 类型系统通过 SendSync Trait 在编译期防止数据竞争

内存安全可视化

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 留下僵尸对象)
  • 无未初始化变量
    • 所有变量使用前必须初始化
  • 无琐碎内存泄漏
    • Drop Trait = 正确实现的 RAII,无需 Rule of Five
    • 编译器在变量离开作用域时自动释放内存
  • 不会忘记解锁互斥锁
    • 锁守卫是访问数据的唯一方式(Mutex<T> 包装数据,而非访问本身)
  • 无异常处理复杂性
    • 错误是值(Result<T, E>),体现在函数签名中,用 ? 传播
  • 出色的类型推断、枚举、模式匹配、零成本抽象支持
  • 内置依赖管理、构建、测试、格式化、lint 支持
    • cargo 替代 make/CMake + lint + 测试框架

快速参考:Rust vs C/C++

概念CC++Rust关键差异
内存管理malloc()/free()unique_ptr, shared_ptrBox<T>, Rc<T>, Arc<T>自动管理,无循环
数组int arr[10]std::vector<T>, std::array<T>Vec<T>, [T; N]默认边界检查
字符串char*\0std::string, string_viewString, &str保证 UTF-8,生命周期检查
引用int* ptrT&, T&&(移动)&T, &mut T借用检查、生命周期
多态函数指针虚函数、继承Trait、trait 对象组合优于继承
泛型编程宏(void*模板泛型 + Trait 约束更清晰的错误信息
错误处理返回码、errno异常、std::optionalResult<T, E>, Option<T>无隐藏控制流
NULL/空值安全ptr == NULLnullptr, std::optional<T>Option<T>强制空值检查
线程安全手动(pthreads)手动同步编译期保证数据竞争不可能发生
构建系统Make、CMakeCMake、Make 等Cargo集成工具链
未定义行为运行时崩溃隐蔽 UB(有符号溢出、别名)编译期错误安全有保证

为什么 C/C++ 开发者需要 Rust

你将学到:

  • Rust 消除的完整问题清单——内存安全、未定义行为(Undefined Behavior)、数据竞争等
  • 为什么 shared_ptrunique_ptr 等 C++ 缓解手段只是创可贴,而非根本解决方案
  • 在 safe Rust 中结构上不可能出现的具体 C/C++ 漏洞示例

想直接看代码? 跳转到 给我看代码

Rust 消除了什么——完整清单

在深入示例之前,先给出执行摘要。Safe Rust 从结构上防止 本清单中的每一个问题——不是靠纪律、工具或代码审查,而是靠类型系统和编译器:

被消除的问题CC++Rust 如何防止
缓冲区溢出 / 下溢所有数组、切片和字符串都携带边界信息;索引在运行时检查
内存泄漏(无需 GC)Drop Trait(特征)= 正确实现的 RAII;自动清理,无需 Rule of Five
悬垂指针生命周期(Lifetime)系统在编译期证明引用比其指向对象活得更久
释放后使用(Use-after-free)所有权(Ownership)系统使其成为编译错误
移动后使用(Use-after-move)移动是破坏性的——原绑定不再存在
未初始化变量所有变量使用前必须初始化;编译器强制检查
整数溢出 / 下溢 UBDebug 构建在溢出时 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_ptrshared_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 使数据竞争成为编译错误
未初始化变量所有变量必须初始化;编译器强制检查
整数 UBDebug 在溢出时 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&lt;T&gt;)<br/>No exceptions (Result&lt;T,E&gt;)<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

概念CC++Rust关键差异
内存管理malloc()/free()unique_ptr, shared_ptrBox<T>, Rc<T>, Arc<T>自动管理,无循环,无僵尸对象
数组int arr[10]std::vector<T>, std::array<T>Vec<T>, [T; N]默认进行边界检查
字符串char*\0std::string, string_viewString, &str保证 UTF-8,生命周期检查
引用int*(原始指针)T&, T&&(移动)&T, &mut T生命周期 + 借用检查
多态函数指针虚函数、继承Trait、trait 对象组合优于继承
泛型宏 / void*模板泛型 + Trait 约束清晰的错误信息
错误处理返回码、errno异常、std::optionalResult<T, E>, Option<T>无隐藏控制流
NULL 安全ptr == NULLnullptr, std::optional<T>Option<T>强制空值检查
线程安全手动(pthreads)手动(std::mutex 等)编译期 Send/Sync数据竞争不可能发生
构建系统Make、CMakeCMake、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 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 工具链有 stablebetanightly(实验性)通道,我们使用 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

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 文件夹下的 debugrelease 子目录
  • 你可能还注意到源码同目录下有 Cargo.lock 文件。它自动生成,不应手动修改
    • 我们稍后会再讨论 Cargo.lock 的具体用途

Rust 内置类型

你将学到: Rust 的基本类型(i32u64f64boolchar)、类型推断、显式类型标注,以及它们与 C/C++ 基本类型的对比。无隐式转换——Rust 要求显式类型转换。

  • Rust 支持类型推断,也允许显式指定类型
说明类型示例
有符号整数i8, i16, i32, i64, i128, isize-1, 42, 1_00_000, 1_00_000i64
无符号整数u8, u16, u32, u64, u128, usize0, 42, 42u32, 42u64
浮点数f32, f640.0, 0.42
Unicodechar‘a’, ‘$’
布尔值booltrue, 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 类型指定与推断

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/elseloop/while/formatch,以及它们与 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 使用 whilefor 的循环

  • 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 顶部
  • 循环标签可与 breakcontinue 配合使用,在处理嵌套循环时很有用

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 的核心数据结构——数组、元组、切片、字符串、结构体、VecHashMap。本章内容较密集;重点理解 String&str 的区别,以及结构体的工作方式。引用与借用将在第 7 章深入讲解。

  • 数组包含固定数量的同类型元素
    • 与 Rust 中其他类型一样,数组默认不可变(除非使用 mut
    • 数组使用 [] 索引,并会进行边界检查。可用 len() 方法获取数组长度
    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。这些格式化器可按类型自定义(后续会介绍)
    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 — 借用、轻量引用(类似带长度的 C const char*,或 C++ 的 std::string_view——但 &str生命周期检查,不会悬垂)
    • 与 C 以 null 结尾的字符串不同,Rust 字符串跟踪长度并保证为有效 UTF-8

面向 C++ 开发者: Stringstd::string&strstd::string_view。与 std::string_view 不同,&str 在其整个生命周期内由借用检查器保证有效。

String&str:拥有 vs 借用

生产实践: 参见 JSON 处理:nlohmann::json → serde,了解生产代码中 serde 与字符串处理的配合。

方面C char*C++ std::stringRust StringRust &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)] 非常实用,几乎应对每个 structenum 都加上。

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 查找(亦称 dictionarymap
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:?}"); 
}

练习:VecHashMap

🟢 入门

  • 创建包含若干条目的 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 TRust 分为共享与独占
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 的 switch
    • match 可用于简单数据类型、structenum 的模式匹配
    • 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! 宏可匹配特定变体
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:?}"),
        _ => (),
    }
}

练习:用 matchenum 实现加减

🟢 入门

  • 编写函数,对无符号 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 可为 structenum 等类型定义关联方法
    • 方法可选地接受 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 的所有权系统——语言中最重要的概念。读完本章你将理解移动语义、借用规则与 Drop Trait。掌握本章,Rust 其余部分会自然跟上。若感到吃力,请重读——多数 C/C++ 开发者在第二遍阅读时才会豁然开朗。

  • C/C++ 的内存管理是 bug 来源:
    • C:用 malloc() 分配、free() 释放。无悬垂指针、释放后使用、双重释放等检查
    • C++:RAII(Resource Acquisition Is Initialization,资源获取即初始化)与智能指针有帮助,但 std::move(ptr) 在移动后仍可编译——移动后使用是 UB
  • 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() 创建独立堆分配。ss1 均有效——各自拥有自己的副本。

Rust Copy Trait

  • Rust 通过 Copy Trait 为内置类型实现拷贝语义
    • 例如 u8、u32、i8、i32 等。拷贝语义使用「按值传递」
    • 用户定义类型可选用 derive 宏自动实现 Copy Trait
    • 新赋值后编译器会为拷贝分配空间
// 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(&amp;str, &amp;str)<br/>→ fn f&lt;'a,'b&gt;(&amp;'a str,<br/>&amp;'b str)"]
    R1 --> R2
    R2["Rule 2: If exactly ONE<br/>input lifetime, assign it<br/>to ALL outputs<br/><br/>fn f(&amp;str) → &amp;str<br/>→ fn f&lt;'a&gt;(&amp;'a str)<br/>→ &amp;'a str"]
    R2 --> R3
    R3["Rule 3: If one input is<br/>&amp;self or &amp;mut self,<br/>assign its lifetime to<br/>ALL outputs<br/><br/>fn f(&amp;self, &amp;str) → &amp;str<br/>→ fn f&lt;'a&gt;(&amp;'a self, &amp;str)<br/>→ &amp;'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 中,生命周期使这种跟踪显式且由编译器验证

CRust发生什么
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&lt;i32&gt;"]
        G["g: Box&lt;i32&gt;"]
    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、浮点)任意类型(StringVec、结构体)
访问模式拷贝进出(.get().set()原地借用(.borrow().borrow_mut()
失败模式不会失败——无运行时检查可变借用时若另有活跃借用会** panic**
开销零——仅拷贝字节小——运行时跟踪借用状态
使用场景不可变结构体内需要可变标志、计数器或小值不可变结构体内需修改 StringVec 或复杂类型

共享所有权: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 无原子开销;跨线程共享才用 Arc
std::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: u64name: StringEmployee 结构体。放入 Rc<Employee> 并 clone 到两个 Vecus_employeesglobal_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 中显式列出

练习:module 与函数

  • 修改 hello world 以调用另一函数
    • 如前所述,函数用 fn 定义。-> 声明函数返回值(默认为 void),类型为 u32(无符号 32 位整数)
    • 函数按 module 作用域,即两个 module 中同名函数不会冲突
      • module 作用域适用于所有类型(例如 mod a { struct foo; } 中的 struct foomod b { struct foo; } 中的 b::foo 是不同类型)

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]
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 与包依赖

  • 查看 hellohellolib 生成的 Cargo.toml。注意两者都已加入上层 Cargo.toml
  • hellolib 中存在 lib.rs 表示库包(定制选项见 https://doc.rust-lang.org/cargo/reference/cargo-targets.html)
  • helloCargo.toml 中添加对 hellolib 的依赖
[dependencies]
hellolib = {path = "../hellolib"}
  • 使用 hellolibadd()
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 依赖与 SemVer

  • crate 可依赖特定版本、特定 minor/major 版本,或不限制。下列示例为在 Cargo.toml 中声明对 rand crate 的依赖
  • 至少 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 test 功能

  • Rust 单元测试通常与源码同文件(按惯例),常分组为独立 module
    • 测试代码从不纳入实际二进制。这由 cfg(配置)特性实现。配置可用于平台特定代码(如 Linux vs Windows
    • 测试用 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 / -O3opt-level0123"s""z"
-fltoltofalse"thin""fat"
-g / 无 -gdebugtruefalse"line-tables-only"
strip 命令strip"none""debuginfo""symbols"true/false
codegen-units1 = 最优优化、编译最慢
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 / CMakeRust build.rs
-lfooprintln!("cargo::rustc-link-lib=foo")
-L/pathprintln!("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-gnurustup 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 -DDEBUGcargo build --features verbose
#ifdef DEBUG#[cfg(feature = "verbose")]
#define MAX 100const 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 testcargo 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 等价
CUnitCMocka、自定义框架内置 #[test] + cargo test
setUp() / tearDown()构建函数 + Drop Trait
#ifdef TEST 模拟函数基于 Trait 的依赖注入
assert(x == y)assert_eq!(x, y),自动 diff 输出
独立测试可执行文件同一二进制,#[cfg(test)] 条件编译
valgrind --leak-check=full ./testcargo test(默认内存安全)+ cargo miri test
代码覆盖率:gcov / lcovcargo tarpaulincargo 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_METHODTrait + 测试 impl更显式,无宏魔法
INSTANTIATE_TEST_SUITE_P(参数化)proptest! 或宏生成测试
SetUp() / TearDown()通过 Drop 的 RAII——清理自动测试结束变量 drop
独立测试二进制 + CMakecargo test——零配置
ctest --output-on-failurecargo 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 的 OptionResult 就是标准库中定义的枚举:
#![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 模式匹配可直接用于 OptionResult
  • 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 是只有两个变体的 enumSome<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 是类似 Optionenum,两个变体: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:同一枚硬币的两面

OptionResult 密切相关——Option<T> 本质上是 Result<T, ()>(错误不带信息的结果):

Option<T>Result<T, E>含义
Some(value)Ok(value)成功——有值
NoneErr(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 运算符 ?match Ok / 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 OptionResult 要点

你将学到: 惯用错误处理模式——unwrap() 的安全替代、? 传播、自定义错误类型,以及生产代码中何时用 anyhow vs thiserror

  • OptionResult 是惯用 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 更好的错误信息
  • Debug 模式下整数溢出:Rust 在溢出时 panic
    • 修复:用 wrapping_add()saturating_add()checked_add() 明确行为
  • String vs &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)
}
}

🟢 自学检查点: 继续前请确认能回答:

  1. 为何 read_to_string 上的 ? 能工作?(因为 #[from] 生成 impl From<io::Error> for ConfigError
  2. 若添加第三变体 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>throwfn 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::AddOutput 关联类型
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::NotRust 用 ! 表示逻辑和按位 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(+ Eqstd::cmp 中,不在 std::ops
operator<PartialOrd(+ Ordstd::cmp
operator<<(流)fmt::Displayprintln!("{}", x)
operator<<(调试)fmt::Debugprintln!("{:?}", x)
operator bool无直接等价物使用 impl From<T> for bool.is_empty() 等命名方法
operator T()(隐式转换)无隐式转换使用 From/Into Trait(显式)

护栏:Rust 阻止了什么

  1. 无隐式转换:C++ 的 operator int() 可能导致静默、令人惊讶的强制转换。Rust 没有隐式转换运算符——使用 From/Into 并显式调用 .into()
  2. 不能重载 && / ||:C++ 允许(破坏短路语义!)。Rust 不允许。
  3. 不能重载 =:赋值始终是移动或复制,从不是用户定义的。复合赋值(+=)可通过 AddAssign 等重载。
  4. 不能重载 ,:C++ 允许 operator,()——最著名的 C++ 陷阱之一。Rust 不允许。
  5. 不能重载 &(取地址):又一个 C++ 陷阱(std::addressof 的存在就是为了绕过它)。Rust 的 & 始终表示「借用」。
  6. 一致性规则:你只能为自有类型实现 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()、接受 u64Log trait
    • 实现两个不同的 logger SimpleLoggerComplexLogger,它们实现 Log trait。一个应输出 “Simple logger” 及 u64,另一个应输出 “Complex logger” 及 u64
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>>需要在集合中混合类型,或插件式可扩展性
enummatch零成本——编译期已知变体是——但仅限已知变体变体集合封闭且在编译期已知
#![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> 特化实现(示例:f32u32
#[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 使用两种不同类型(TU
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,接受任何实现了 ComputeArea trait 的类型 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 约束可与泛型结合用于数据类型
  • 在以下示例中,我们定义 PrintDescription trait 和泛型 struct Shape,其成员受 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、且该成员实现 CipherTextstruct
#![allow(unused)]
fn main() {
trait CipherText {
    fn encrypt(&self);
}
// TO DO
//struct Cipher<>

}
  • 接下来,在 structimpl 上实现名为 encrypt 的方法,调用 cipher 上的 encrypt
#![allow(unused)]
fn main() {
// TO DO
impl for Cipher<> {}
}
  • 接下来,在两个名为 CipherOneCipherTwo 的结构体上实现 CipherTextprintln() 即可)。创建 CipherOneCipherTwo,并用 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,假设有两种状态:IdleFlying。在 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 标记数据类型。此处我们用它表示 IdleFlying 状态,但运行时大小为 zero
  • 注意 takeoffland 方法将 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 类型状态模式泛型

  • 要点:
    • 状态可用结构体表示(零大小)
    • 可将状态 TPhantomData<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>,用于可能失败的 TryFromTryInto。实现 From 即可免费获得 Into。替代 C++ 转换运算符和构造函数。

  • FromInto 是便于类型转换的互补 Trait
  • 类型通常实现 From Trait。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 实现 From Trait,转换为名为 TransposePoint 的类型。TransposePoint 交换 Pointxy 元素
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

  • Default Trait 有多种用途,包括
    • 执行部分复制,其余用默认初始化
    • 作为 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(FnFnMutFnOnce)、move 闭包,以及 Rust 闭包与 C++ lambda 的对比——自动捕获分析,而非手动的 [&]/[=] 指定。

  • 闭包是可以捕获其环境的匿名函数
    • C++ 等价物:lambda([&](int x) { return x + 1; }
    • 关键差异:Rust 闭包有三种捕获 Trait(FnFnMutFnOnce),由编译器自动选择
    • C++ 捕获模式([=][&][this])是手动的且容易出错(悬垂的 [&]!)
    • Rust 的借用检查器在编译期防止悬垂捕获
  • 闭包可通过 || 符号识别。类型参数包含在 || 内,可使用类型推断
  • 闭包常与迭代器配合使用(下一主题)
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}");
}
}

windowschunks——切片上的滑动/固定大小视图

#![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++ 等价物:手动索引运算 ii+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)>(名称、温度),编写一条 迭代器链:

  1. 过滤温度 > 80.0 的传感器
  2. 按温度降序排序
  3. 将每个格式化为 "{name}: {temp}°C [ALARM]"
  4. 收集为 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 迭代器

  • Iterator Trait 用于为用户定义类型实现迭代(https://doc.rust-lang.org/std/iter/trait.IntoIterator.html)
    • 在示例中,我们将为斐波那契数列实现迭代器,它以 1, 1, 2, … 开始,后继数是前两个数之和
    • Iterator 中的 associated typetype Item = u32;)定义迭代器的输出类型(u32
    • next() 方法包含实现迭代器的逻辑。此例中,所有状态信息都在 Fibonacci 结构体中
    • 我们也可以实现名为 IntoIterator 的另一个 Trait,为更专门的迭代器实现 into_iter() 方法
    • ▶ 在 Rust Playground 中尝试

迭代器利器

迭代器强力工具参考

你将学到: 超越 filter/map/collect 的高级迭代器组合子——enumeratezipchainflat_mapscanwindowschunks。对于用安全、表达力强的 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]
}

windowschunks——滑动与固定大小分组

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
}

foldscan——累加

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"),编写 迭代器管道:

  1. 将每行解析为 (name, f64, unit)
  2. 过滤低于阈值的读数
  3. 使用 fold 按传感器名称分组到 HashMap
  4. 打印每个传感器的平均读数
// 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 迭代器

  • Iterator Trait 用于为用户定义类型实现迭代(https://doc.rust-lang.org/std/iter/trait.IntoIterator.html)
    • 在示例中,我们将为斐波那契数列实现迭代器,它以 1, 1, 2, … 开始,后继数是前两个数之和
    • Iterator 中的 associated typetype 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 通过 SendSync 标记 Trait 在编译期防止数据竞争
    • 在 C++ 中,在没有 mutex 的情况下跨线程共享 std::vector 是 UB 但能编译通过。在 Rust 中,无法编译。
    • Rust 中的 Mutex<T> 包装的是数据,而不只是访问——不锁定就无法读取数据
  • 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 中毒

  • 如果线程在持有 MutexRwLockpanic,锁会变为中毒状态
    • 后续对 .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 的开销
    • AtomicBoolAtomicI32AtomicU64AtomicUsize
    • 等价于 C++ 的 std::atomic<T>——相同的内存序模型(RelaxedAcquireReleaseSeqCst
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 通道可用于在 SenderReceiver 之间交换消息
    • 这使用称为 mpscMulti-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:若类型可安全转移到另一线程,则该类型是 Send
    • Sync:若类型可通过 &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(借用):

类型类比何时使用
CStringString(拥有)从 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 更精确:

MiriValgrindC++ 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(查阅详尽文档)
  • 不要假设你的 unsafe 代码是正确的,或从 Safe Rust 使用它没问题。很容易犯错,即使看似正确的代码也可能因微妙原因而出错
    • 使用工具验证正确性
    • 若仍有疑问,寻求专家意见
  • 确保 unsafe 代码有明确记录假设及为何正确的注释
    • unsafe 代码的调用者也应有相应的 safety 注释,并遵守限制

练习:编写 Safe Rust FFI 包装

🔴 挑战——需要理解 unsafe 块、裸指针和 Safe Rust API 设计

  • 围绕 unsafe FFI 风格函数编写 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——corealloc crate 的分层、panic handler,以及与无 libc 的嵌入式 C 的对比。

如果你来自嵌入式 C,你已经习惯在没有 libc 或仅有极简运行时的情况下工作。Rust 有对应的一等公民方案:#![no_std] 属性。

什么是 no_std

在 crate 根添加 #![no_std] 后,编译器会移除隐式的 extern crate std;,仅链接 core(以及可选的 alloc)。

层级提供什么需要 OS / 堆?
core基本类型、OptionResultIterator、数学、slicestr、原子操作、fmt — 可在裸机上运行
allocVecStringBoxRcArcBTreeMap需要全局分配器,但不需要 OS
stdHashMapfsnetthreadioenvprocess — 需要 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
HashMapheapless::FnvIndexMap(固定容量)或 BTreeMap(配合 alloc
Vecheapless::Vec(栈分配、固定容量)
Stringheapless::String&str
std::io::Read/Writeembedded_io::Read/Write
thread::spawn中断处理程序、RTIC 任务
std::time硬件定时器外设
std::fsFlash / EEPROM 驱动

嵌入式常用 no_std crate

Crate用途说明
heapless固定容量 VecStringQueueMap无需分配器 — 全部在栈上
defmt经 probe/ITM 的高效日志类似 printf,格式化延迟到主机端
embedded-hal硬件抽象 Trait(SPI、I²C、GPIO、UART)实现一次,可在任意 MCU 上运行
cortex-mARM Cortex-M intrinsic 与寄存器访问底层,类似 CMSIS
cortex-m-rtCortex-M 运行时 / 启动代码替代你的 startup.s
rtic实时中断驱动并发(Real-Time Interrupt-driven Concurrency)编译期任务调度,零开销
embassy嵌入式异步执行器裸机上的 async/await
postcardno_std serde 序列化(二进制)无法用字符串时替代 serde_json
thiserrorError Trait 派生宏v2 起支持 no_std;优先于 anyhow
smoltcpno_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_stdstd

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) -> usize
  • is_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.scortex-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&lt;I2C: I2c&gt;"]
        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 提供 VecStringBox — 但你需要告诉 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 实现。

要求:

  1. 定义 LedController<SPI> 结构体
  2. 实现 new()set_brightness(led: u8, brightness: u8)all_off()
  3. SPI 协议:发送 2 字节事务 [led_index, brightness_value]
  4. 使用 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 + GDBprobe-rs
安装2 个独立包 + 脚本cargo install probe-rs-tools
配置每板/探针的 .cfg 文件--chip 标志或 Embed.toml
控制台输出Semihosting(很慢)RTT(约快 10 倍)
日志框架printfdefmt(结构化、零成本)
烧录算法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 与 printflog

特性C printf(semihosting)Rust log cratedefmt
速度每次调用约 100msN/A(需要 std每次调用约 1μs
Flash 占用完整格式字符串完整格式字符串仅索引(字节)
传输Semihosting(暂停 CPU)串口/UARTRTT(非阻塞)
结构化输出仅文本类型化、二进制编码
no_std通过 semihosting仅门面(后端需要 std✅ 原生
过滤级别手动 #ifdefRUST_LOG=debugdefmt::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.cfgprobe-rs info(自动检测探针 + 芯片)
arm-none-eabi-gdb -x .gdbinitprobe-rs gdb --chip STM32F401RE
target remote :3333GDB 连接 localhost:1337
monitor reset haltprobe-rs reset --chip ...
load firmware.elfcargo flash --chip ...
printf("debug: %d\n", val)(semihosting)defmt::info!("debug: {}", val)(RTT)
Keil/IAR GUI 调试器VS Code + probe-rs-debugger 扩展
Segger SystemViewdefmt + 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
2shared_ptr / enable_shared_from_thisArena + 索引链接无引用循环
3每个模块中的 Framework* 裸指针带生命周期借用的 DiagContext<'a>编译期有效性
4God object可组合状态结构体可测试、模块化
5处处 vector<unique_ptr<Base>>仅在需要处使用 Trait 对象(约 25 处)默认静态分发

前后指标

指标C++(原始)Rust(重写)
dynamic_cast / 类型向下转型约 4000
virtual / override 方法约 900约 25(Box<dyn Trait>
new 分配约 2000(全部为 owned 类型)
shared_ptr / 引用计数约 10(拓扑库)0(仅在 FFI 边界使用 Arc
enum class 定义约 60约 190 个 pub enum
模式匹配表达式N/A约 750 个 match
God object(>5K 行)20

案例研究 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_triggers vs m_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>无需修改框架即可添加新模块
测试 mockBox<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

翻译指标与经验总结

我们学到了什么

  1. 默认使用枚举分发 — 在约 10 万行 C++ 中,仅约 25 处 Box<dyn Trait> 真正必要(插件系统、测试 mock)。其余约 900 个虚方法变为带 match 的枚举
  2. Arena 模式消除引用循环shared_ptrenable_shared_from_this 是所有权不清的症状。先想清楚谁拥有数据
  3. 传递上下文,不要存储指针 — 生命周期有界的 DiagContext<'a> 比在每个模块里存 Framework* 更安全、更清晰
  4. 分解 god object — 若结构体有 30+ 字段,多半是 3–4 个结构体穿同一件大衣
  5. 编译器是你的结对伙伴 — 约 400 次 dynamic_cast 意味着约 400 次潜在运行时失败。Rust 中零 dynamic_cast 等价物意味着零运行时类型错误

最难的部分

  • 生命周期注解:习惯了裸指针后,把借用写对需要时间 — 但一旦编译通过,就是对的
  • 与借用检查器较劲:想在两处同时持有 &mut self。解法:将状态分解为独立结构体
  • 抵制字面翻译:处处写 Vec<Box<dyn Base>> 的诱惑。问自己:「这组变体是封闭的吗?」→ 若是,用枚举

给 C++ 团队的建议

  1. 从小而独立的模块开始(不要先动 god object)
  2. 先翻译数据结构,再翻译行为
  3. 让编译器引导你 — 错误信息非常出色
  4. dyn Trait 之前先考虑 enum
  5. 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

  • 堆数据类型不要实现 CopyStringVecHashMap
  • 值可能为 NaN 时不要实现 Eq:含 f32/f64 的类型
  • 没有合理默认值时不要实现 Default:文件句柄、网络连接
  • clone 昂贵时不要实现 Clone:大型数据结构(考虑 Rc<T>

摘要:Trait 收益

Trait收益何时使用
Debugprintln!("{:?}", value)几乎总是(极少数例外)
Displayprintln!("{}", value)面向用户的类型
Clonevalue.clone()显式复制有意义时
Copy隐式复制小型简单类型
PartialEq==!=大多数类型
Eq自反相等数学上相等可靠时
PartialOrd<><=>=有自然序的类型
Ord.sort()BinaryHeap全序时
HashHashMap用作 map 键的类型
DefaultDefault::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 Stringself.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 吗?

  1. 能否接受 &str / &T 而非 String / T → 借用,不要 clone
  2. 能否重构以避免两个所有者? → 传引用或用作用域
  3. 这是 Arc::clone() 吗? → 可以,O(1)
  4. 要把数据移入线程/闭包? → clone 必要
  5. 在热循环里 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 或 nullWeak::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 用 PartialEq vs Eq 编码这一点。

Copy vs Clone

CopyClone
如何工作按位 memcpy(隐式)自定义逻辑(显式 .clone()
何时发生赋值:let b = a;仅调用 .clone()
复制/clone 后ab 均有效ab 均有效
既无 Copy 也无 Clonelet b = a; 移动 aa 消失)let b = a; 移动 aa 消失)
允许用于无堆数据的类型任意类型
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

PartialEqEq
提供什么==!=标记:「相等是自反的」
自反?(a == a)不保证保证
为何重要f32::NAN != f32::NANHashMap要求 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

PartialOrdOrd
提供什么<><=>=.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, DefaultFanStatus
用作 HashMap 键的枚举Copy, Clone, PartialEq, Eq, HashCpuFaultTypeSelComponent
可排序严重级别枚举Copy, Clone, PartialEq, Eq, PartialOrd, OrdFaultSeverityGpuDiagLevel
含 String 的数据结构体Clone, Debug, Serialize, DeserializeFruDataOverallSummary
可序列化配置Clone, Debug, Default, Serialize, DeserializeDiagConfig

避免未检查索引

避免未检查索引

你将学到: 为何在 Rust 中 vec[i] 很危险(越界会 panic),以及 .get()、迭代器、HashMapentry() 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")带消息的 panicpanic 合理时,说明原因
.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

  • OptionResult 上的这些方法让你在不 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 + mapif + break 的循环
.filter(|v| ...)Option / Iterator只保留满足谓词的值if (!predicate) return nullopt;
.ok()?ResultResult → Option 并传播 Noneif (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 融合了 findmap:在第一个匹配处停止并变换。C++ 等价物是带 if + breakfor 循环。

真实示例:用 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)]将未知键吸收进 HashMapfor (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 derivecargo 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 代码中的模式。

要求:

  1. 定义带 Displayenum Severity { Info, Warning, Critical },以及包含 source: Stringseverity: Severitymessage: Stringfault_code: u32struct DiagEvent
  2. 定义带方法 fn should_include(&self, event: &DiagEvent) -> booltrait EventFilter
  3. 实现两个过滤器:SeverityFilter(仅包含 >= 给定严重级别的事件)和 SourceFilter(仅包含来自特定源字符串的事件)
  4. 编写函数 fn process_events(events: &[DiagEvent], filters: &[&dyn EventFilter]) -> Vec<String>,返回通过全部过滤器的事件的格式化报告行
  5. 编写 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 的双层日志架构(门面 + 后端)、logtracing crate、带 span 的结构化日志,以及如何替代 printf/syslog 调试。

C++ 诊断代码通常使用 printfsyslog 或自定义日志框架。 Rust 有标准化的双层日志架构:门面 crate(logtracing)与后端(实际日志实现)。

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 的结构化日志

tracinglog 基础上扩展了结构化字段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())
}
}

logtracing — 选哪个

方面logtracing
复杂度简单 — 5 个宏更丰富 — span、字段、instrument
结构化数据仅字符串插值键值字段:info!(gpu_id = 0, "msg")
计时 / span有 — #[instrument]span.enter()
异步支持基础一等公民 — span 跨 .await 传播
兼容性通用门面log 兼容(有 log 桥接)
适用场景简单应用、库诊断工具、异步代码、可观测性

建议:生产级诊断类项目(带结构化输出的诊断工具)用 tracing。 依赖要少的简单库用 logtracing 含兼容层,使用 log 宏的库在 tracing subscriber 下仍可工作。

后端选项

后端 Crate输出适用场景
env_loggerstderr,彩色开发、简单 CLI 工具
tracing-subscriberstderr,格式化生产环境配合 tracing
syslog系统 syslogLinux 系统服务
tracing-journaldsystemd journalsystemd 管理的服务
tracing-appender轮转日志文件长期运行的守护进程
tracing-opentelemetryOpenTelemetry 收集器分布式追踪

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安全,返回 Resultlet i: u8 = 300_u16.try_into()?; — 返回 Err
dynamic_cast(向下转换)对枚举 match / Any::downcast_ref安全枚举用模式匹配;trait 对象用 Any
const_cast无等价物Rust 在 Safe Rust 中无法将 & 转为 &mut。内部可变性用 Cell/RefCell
reinterpret_caststd::mem::transmuteunsafe重新解释位模式。几乎总是错的 — 优先 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 常量 → constconst 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.14const 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) 供父模块访问
publicpub到处可访问
friend class FooFoo 放在同一模块模块级隐私替代 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 等价说明
硬件寄存器用 volatileptr::read_volatile / ptr::write_volatile需要 unsafe — 适用于 MMIO
线程信号用 volatileAtomicBool / AtomicU32C++ 用 volatile 做这也错!
std::atomic<T>std::sync::atomic::AtomicT语义相同,顺序相同
std::atomic<T>::load(memory_order_acquire)AtomicT::load(Ordering::Acquire)1:1 映射

static 变量 → staticconstLazyLockOnceLock

基本 staticconst

// 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_localthread_local! { static X: Cell<u32> = Cell::new(0); }语义相同

constexprconst fn

C++ constexpr 标记函数与变量供编译期求值。Rust 用 const fnconst 达到同样目的:

// 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 初始化的 staticRust static 默认必须 const 初始化

const fn 的当前限制(截至 Rust 1.82 已稳定):

  • 无 trait 方法(const 上下文中不能对 Vec 调用 .len()
  • 无堆分配(Box::newVec::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 concepttrait🟢 意图几乎相同
requires 子句where 子句🟢 位置与语法相似
模板深处编译失败调用点 trait 不匹配即失败🟢 无 200 行错误级联

要点:C++ concepts(C++20)最接近 Rust trait。 若熟悉 C++20 concepts,可把 Rust trait 视为自 1.0 起就是一等语言特性的 concepts, 有一致的实现模型(trait impl)而非鸭子类型。


std::function → 函数指针、impl FnBox<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::functionBox<dyn FnMut(Args) -> Ret>
一次性回调(被消费)移动的 std::functionBox<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>无等价物VecVecDeque
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::stringString保证 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::multimapstd::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 Baseenum Variant { A {...}, B {...} }封闭集合优先用枚举
virtual void method() = 0trait 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_pFrameworkfn 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 + lintcargo build / test / clippy / fmt一个工具搞定一切

迁移策略

  1. 从数据类型开始:先翻译结构体与枚举 — 迫使你思考所有权
  2. 工厂转枚举:若工厂创建不同派生类型,多半应是 enum + match
  3. 上帝对象拆成组合结构体:将相关字段分组到专注的结构体
  4. 指针换借用:将存储的 Base* 转为带生命周期的 &'a T 借用
  5. 谨慎使用 Box<dyn Trait>:仅用于插件系统与测试 mock
  6. 让编译器引导你: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]用元素创建 VecVec::from([1, 2, 3])(近似)
todo!()标记未完成代码panic!("not yet implemented")
unimplemented!()标记故意未实现代码panic!("not implemented")
unreachable!()标记编译器无法证明不可达的代码panic!("unreachable")
assert!(cond)条件为假则 panicif !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根据目标为 truefalse
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 键的类型
DefaultType::default() 构造器有合理零/空值的类型
serde::Serialize / DeserializeJSON/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)] — 为错误枚举生成 DisplayFrom impl
  • serde#[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::mina.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!");
}