About Hello Rust

Important

最好的学习方法是间隔性重复学习。

一个编程高手是怎样练成的呢? 惟手熟尔。重在刻意练习。 这意为着,就是不断重复练习,实践,再实践,熟练掌握各种技能。因为,只有反复练习,才能真正掌握。

Hello,Rust 是如何产生的呢? 这是我在学习Rust过程中,不断地编写样例代码,不断点滴积累经验,最终形成的。

Rust 是一个非常优秀的系统编程语言,它简洁易读,性能高,更安全,功能强大。然而,它也存在学习曲线陡峭的问题,需要花费大量时间和精力去理解借用检查、所有权机制等新的编程概念,确实不太容易上手。

对于新手来说,Hello, Rust 是一个绝佳的起点。通过这个项目,你不仅能快速入门 Rust 编程,还能通过编程、调试、运行示例代码,迅速掌握 Rust 的核心知识点,熟悉基础语法和基本概念。更棒的是,它还涵盖了高级进阶知识和精选的 Rust Crates 库应用示例。

本书的当前版本假设你使用 Rust 1.94.0 或更高版本并在所有项目的 Cargo.toml 文件中通过 edition = "2024" 将其配置为使用 Rust 2024 edition 惯用法。请查看Getting Started的 “安装” 部分了解如何安装和升级 Rust。

Introduction

Rust 是一种现代、高性能的编程语言,专注于安全性、并发性和速度。由 Mozilla 开发,现由 Rust 基金会维护,广泛应用于系统编程、Web 后端和高性能应用。

Rust 是一种通用的编程语言,强调性能、类型安全和并发性。它强制执行内存安全,确保所有引用都指向有效内存。与传统的垃圾回收机制不同,Rust 通过“借用检查器”在编译时跟踪引用的对象生命周期,从而防止内存安全错误和数据竞争。

Rust 支持多种编程范式。它受到函数式编程思想的影响,包括不可变性、高阶函数、代数数据类型和模式匹配。同时,它通过结构体、枚举、特性和方法支持面向对象编程。

为什么选择 Rust?

  • 安全性:Rust 的“所有权和借用”,通过“借用检查器“在编译时检测内存安全问题,确保了内存安全。
  • 性能:Rust 通过追求零成本抽象(zero-cost abstractions)—— 将高级语言特性编译成底层代码,并且与手写的代码运行速度同样快。Rust 努力确保代码又安全又快速。Rust 的编译器可以生成高效的机器码,接近C的性能。
  • 并发性:Rust 提供了强大的并发模型,支持异步编程,多线程编程,async/await 异步原语,tokio/async-std 异步运行时。
  • 优秀的包管理:Cargo提供Rust 强大的包管理工具, 包版本,依赖,测试一套工具全套方案解决,尤其体验过C/C++ 包管理痛点的人,使用Cargo 感觉太便捷了。
  • 可读性:Rust 的代码风格统一简洁易读。rustfmt 帮你整理代码格式,统一代码风格,便捷阅读。clippy 帮你检查代码问题。

Getting Started

安装 Rust

首先,你需要安装 Rust。你可以从 Rust 官方网站 下载并安装。

Windows 上,你可以从 Rust 官方网站 下 载并安装。

Linux 上,你可以使用包管理器来安装 Rust:

    $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    $ rustup update
    # rustup toolchain install stable
    $ rustup default stable

或者 Ubuntu 下使用以下命令来安装 Rust:

    $ sudo apt-get install rustup
    $ rustup update
    # rustup toolchain install stable
    $ rustup default stable

MacOS 上,你可以使用 Homebrew 来安装 Rust:

    $ brew install rustup
    $ rustup update
    # rustup toolchain install stable
    $ rustup default stable

创建项目

安装完成后,你可以使用以下命令来创建一个新的 Rust项目:

    $ cargo new hello-rust

这将创建一个名为 hello-rust 的项目目录,并在其中创建一个 Cargo.toml 文件,其中包含了项目的依赖信息。 进入项目目录:

    $ cd hello-rust

你应该会看到以下目录结构:

.
├── Cargo.toml
└── src
    └── main.rs

现在你可以编辑 src/main.rs 文件来编写你的 Rust 代码。cargo 会默认生成一个 main.rs 文件,并在其中包含以下代码:

fn main() {
    println!("Hello, world!");
}

main.rs 是一个 Rust 程序的入口点。

或者你可以使用cargo new -lib hello-rust命令,创建的是一个crates库。Cargo 会创建一个lib.rs文件,它包含库的入口点,并且可以被其他 Rust 项目导入和使用。

Cargo.toml 文件内容如下:

[package]
name = "hello-rust"
version = "0.1.0"
edition = "2024"

[dependencies]

Cargo.toml 文件中,

  • [package] 部分定义了项目的名称、版本和 Rust 版本。
  • [dependencies] 部分定义了项目的依赖。在这个例子中,我们没有添加任何依赖,所以我们不需要添加任何依赖。

编译和运行

你可以使用以下命令来编译和运行项目

    $ cargo build
    $ cargo run

这将编译项目并运行 main.rs 文件。运行后,你会看到输出 Hello, world!

一个完整的 Rust 项目结构

Cargo 推荐的目录结构,如下:

  • Cargo.toml 和 Cargo.lock 保存在 package 根目录下
  • 源代码放在 src 目录下
  • Crate子模块源代码放在 crates 目录下
  • 默认的 lib 包根是 src/lib.rs
  • 默认的二进制包根是 src/main.rs
    • 其它二进制包根放在 src/bin/ 目录下
  • 基准测试 benchmark 放在 benches 目录下
  • 示例代码放在 examples 目录下
  • 集成测试代码放在 tests 目录下

测试你的代码

tip

良好的编程习惯,一定要写单元测试。下面先认识下,如何编写一个简单的单元测试,后面会有单独的章节来详细介绍如何编写单元测试。可以先使用Copy的技能,照着样例去写,然后慢慢深入理解。接下来去测试你的第一个 Rust 程序吧。

单元测试的结构

单元测试通常包含以下部分:

  1. 导入模块:使用 use 语句导入需要的模块。
  2. 定义测试函数:使用 #[test] 注解定义测试函数。测试函数应该以 fn 开头,并且返回 ResultOption 类型。
  3. 编写测试代码:在测试函数中编写实际的测试代码。你可以使用断言来验证函数的行为。
  4. 运行测试:使用 cargo test 命令来运行测试。

示例:单元测试


fn main() {
    println!("Hello, world!");
}

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

cargo test 会运行 tests 目录下的所有测试文件。你可以使用以下命令来编译和运行测试,运行上述命令后,你会看到以下输出:

running 1 test
test tests::it_works ... ok

successes:

successes:
    tests::it_works

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

说明你的单元测试通过了。

在这个例子中,add 函数的测试通过了。如果 add 函数返回的值不是 4,测试将会失败。你可以通过修改 add 函数的返回值来验证这一点。

caution

如果你的测试失败了,你可以通过查看 test 目录下的输出文件来找到具体的错误信息。

running 1 test
test tests::it_works ... FAILED

successes:

successes:

failures:

---- tests::it_works stdout ----

thread 'tests::it_works' panicked at src/main.rs:16:9:
assertion `left == right` failed
  left: 4
 right: 5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/panicking.rs:695:5
   1: core::panicking::panic_fmt
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/panicking.rs:75:14
   2: core::panicking::assert_failed_inner
   3: core::panicking::assert_failed
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/panicking.rs:380:5
   4: hello_rust::tests::it_works
             at ./src/main.rs:16:9
   5: hello_rust::tests::it_works::{{closure}}
             at ./src/main.rs:14:18
   6: core::ops::function::FnOnce::call_once
             at /Users/weirenyan/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
   7: core::ops::function::FnOnce::call_once
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    tests::it_works

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

error: test failed, to rerun pass `-p hello-rust --bin hello-rust`

Rust 返回的测试失败结果信息,是很详细的,所以你一定要详细阅读错误信息,看清楚问题所在。最好的方法是通过错误问题,调试代码并解决这些问题,最终可以成功编译和运行项目,整体过程能快速提升代码能力。

note

经过上述简单的旅程,我们已经对 Rust 有了初步的了解。接下来,我们将深入探索 Rust 的核心概念和特性。那么,让我们继续前进吧!开始进入 Rust 的世界旅行吧!

基础入门 (Basic)

📖 学习内容概览

欢迎来到 Rust 编程之旅的第一站!基础入门部分将带你掌握 Rust 的核心概念和编程范式。这些知识是后续所有高级主题的基石。


🎯 你将学到什么

完成本部分学习后,你将能够:

  1. 理解 Rust 所有权系统 - Rust 最核心、最独特的概念
  2. 掌握借用和生命周期 - 安全地共享数据
  3. 使用结构体和枚举 - 组织复杂数据
  4. 编写泛型和特征 - 实现可复用的代码
  5. 理解并发基础 - 安全的多线程编程

📚 章节列表

章节说明难度预计时间
变量与表达式Rust 变量绑定、可变性、作用域🟢 简单30 分钟
基础数据类型标量类型、复合类型、类型转换🟢 简单45 分钟
了解所有权所有权规则、移动语义、作用域🟡 中等60 分钟
结构体定义、实例化、方法🟡 中等45 分钟
结构体字段字段定义、初始化、使用🟡 中等30 分钟
结构体方法impl 块、关联函数、方法🟡 中等30 分钟
枚举定义、模式匹配、Option🟡 中等45 分钟
特征 (Traits)定义、实现、特征对象🟡 中等60 分钟
泛型泛型函数、泛型结构体、约束🟡 中等45 分钟
闭包闭包语法、捕获环境、Fn trait🟡 中等45 分钟
模块系统包、模块、路径、可见性🟡 中等45 分钟
线程与并发线程、消息传递、共享状态🔴 困难60 分钟
条件编译条件编译、平台特定代码🟢 简单30 分钟
指针与不安全代码原始指针、unsafe 代码🔴 困难60 分钟
日志记录env_logger、日志级别🟢 简单30 分钟
追踪 (Tracing)tracing、span、事件追踪🟡 中等45 分钟
可见性控制pub、私有、模块可见性🟢 简单30 分钟

🔗 前置要求

无需 Rust 基础! 本部分从零开始教学。

建议具备:

  • 基本编程概念(变量、循环、函数)
  • 任何编程语言经验(Python、Java、JavaScript 等)

📈 学习路径

变量与表达式 → 数据类型 → 所有权 → 结构体 → 枚举 → 特征 → 泛型 → 闭包 → 模块 → 线程 → 条件编译 → 指针 → 日志 → 追踪 → 可见性

✅ 学习检查点

完成本部分后,你应该能够:

  • 解释所有权三规则
  • 区分借用和所有权
  • 使用结构体和枚举组织数据
  • 为结构体实现方法
  • 使用特征实现多态
  • 编写泛型函数和结构体
  • 理解 Rust 模块系统
  • 使用闭包捕获环境
  • 创建基本的多线程程序

🎓 实践项目

建议练习:

  1. 创建一个简单的命令行工具
  2. 实现一个数据结构(如待办事项列表)
  3. 编写多线程程序计算数据

➡️ 下一步

完成基础入门后,继续学习 高级进阶 部分,你将学习:

  • 异步编程 (Async/Await)
  • 数据库操作 (SQLx, Diesel)
  • Web 框架 (Axum, Hyper)
  • 序列化 (Serde, JSON)
  • 以及更多高级主题!

准备好了吗?让我们开始 变量与表达式 的学习! 🚀


📚 完整章节列表

序号章节难度预计时间
1变量与表达式🟢 简单30 分钟
2基础数据类型🟢 简单45 分钟
3了解所有权🟡 中等60 分钟
4结构体🟡 中等45 分钟
4.1结构体字段🟡 中等30 分钟
4.2结构体方法🟡 中等30 分钟
5枚举🟡 中等45 分钟
6特征 (Traits)🟡 中等60 分钟
7泛型🟡 中等45 分钟
8闭包🟡 中等45 分钟
9模块系统🟡 中等45 分钟
10线程与并发🔴 困难60 分钟
11条件编译🟢 简单30 分钟
12指针与不安全代码🔴 困难60 分钟
13日志记录🟢 简单30 分钟
14追踪 (Tracing)🟡 中等45 分钟
15可见性控制🟢 简单30 分钟

总计: 15 章 | 总时间: 约 13 小时


📈 学习路径图

1. 变量与表达式
   ↓
2. 基础数据类型
   ↓
3. 了解所有权
   ↓
4. 结构体 ─┬─ 4.1 结构体字段
           └─ 4.2 结构体方法
   ↓
5. 枚举
   ↓
6. 特征 (Traits)
   ↓
7. 泛型
   ↓
8. 闭包
   ↓
9. 模块系统
   ↓
10. 线程与并发
    ↓
11. 条件编译
    ↓
12. 指针与不安全代码
    ↓
13. 日志记录
    ↓
14. 追踪 (Tracing)
    ↓
15. 可见性控制
    ↓
    🎓 毕业 → 高级进阶

✅ 学习检查点

完成本部分后,你应该能够:

  • 解释所有权三规则
  • 区分借用和所有权
  • 使用结构体和枚举组织数据
  • 为结构体实现方法
  • 使用特征实现多态
  • 编写泛型函数和结构体
  • 理解 Rust 模块系统
  • 使用闭包捕获环境
  • 创建基本的多线程程序

🎓 实践项目

建议练习:

  1. 创建一个简单的命令行工具
  2. 实现一个数据结构(如待办事项列表)
  3. 编写多线程程序计算数据

➡️ 下一步

完成基础入门后,继续学习 高级进阶 部分,你将学习:

  • 异步编程 (Async/Await)
  • 数据库操作 (SQLx, Diesel)
  • Web 框架 (Axum, Hyper)
  • 序列化 (Serde, JSON)
  • 以及更多高级主题!

准备好了吗?让我们开始 变量与表达式 的学习! 🚀

知识检查

快速测验(答案在下方):

  1. Rust 是什么类型的语言?

  2. Rust 的主要优势是什么?

  3. 本教程的学习路径是什么?

点击查看答案与解析
  1. Rust 是系统级编程语言,注重内存安全和性能
  2. 内存安全无需 GC、零成本抽象、并发安全
  3. 基础入门 → 高级进阶 → 实战精选 → 算法练习

关键理解: Rust 是一门独特的语言,结合了系统级性能和现代语言安全性。

延伸阅读

完成基础入门后,你可能还想了解:

选择建议:

变量与表达式

开篇故事

想象你有一个工具箱,里面装着各种工具:螺丝刀、锤子、尺子。你给每个工具贴上标签,下一次需要时就知道去哪里找。Rust 中的变量就像这些贴标签的工具箱 - 它们帮你存储和管理程序中的数据。表达式则是你使用这些工具完成的工作。


本章适合谁

如果你是 Rust 初学者,想理解如何存储数据、进行计算和控制程序流程,本章适合你。这是所有编程的基础,即使你是第一次接触编程也能理解。


你会学到什么

完成本章后,你可以:

  1. 使用 let 关键字声明和初始化变量
  2. 理解不可变性(immutability)和可变性(mutability)的区别
  3. 区分变量和常量
  4. 使用表达式进行数值和位运算
  5. 编写条件语句和循环表达式

前置要求

本章是 Rust 的第一章,不需要前置知识。如果你有任意编程基础(Python、JavaScript、Java 等)会更容易理解。


第一个例子

让我们从最简单的变量声明开始:

fn main() {
    let x = 5;  // 声明一个不可变变量
    let mut y = 10;  // 声明一个可变变量
    
    println!("x 的值是:{}", x);  // 输出:5
    println!("y 的值是:{}", y);  // 输出:10
    
    y = 15;  // ✅ 可以修改 y
    // x = 10;  // ❌ 编译错误!x 是不可变的
    
    println!("y 的新值是:{}", y);  // 输出:15
}

关键概念

  • let - 声明变量的关键字
  • mut - 使变量可变(mutable 的缩写)
  • 默认情况下,Rust 变量是不可变

原理解析

1. 变量绑定 (Variable Binding)

在 Rust 中,声明变量称为"绑定" - 你将一个名称绑定到一个值上:

#![allow(unused)]
fn main() {
let apples = 5;  // 将名称 "apples" 绑定到值 5
}

为什么叫绑定?

  • 不同于其他语言的"赋值"(assignment)
  • Rust 的绑定是一次性的(除非使用 mut
  • 绑定后,名称和值关联在一起

2. 不可变性 (Immutability)

Rust 默认让变量不可变:

let x = 5;
x = 6; // ❌ 编译错误!

为什么 Rust 要这样设计?

  1. 安全性:防止意外的数据修改
  2. 并发安全:不可变数据可以安全地在线程间共享
  3. 更容易推理:知道值不会改变,代码更容易理解

类比

就像你写在纸上的数字 - 写完后就不能改变。如果你想改,需要拿一张新纸重写。

3. 可变性 (Mutability)

当你需要修改变量时,使用 mut

#![allow(unused)]
fn main() {
let mut counter = 0;
counter = 1;  // ✅ 可以修改
counter += 1; // ✅ 也可以这样累加
}

注意:只在需要修改时使用 mut,这是 Rust 的最佳实践。

4. 常量 (Constants)

常量是永远不变的值:

#![allow(unused)]
fn main() {
const MAX_USERS: u32 = 1000;
const PI: f64 = 3.14159;
}

常量 vs 不可变变量

特征不可变变量 (let)常量 (const)
关键字letconst
类型注解可选必须
编译时确定
生命周期作用域内整个程序运行期间
内存地址运行时分配编译时嵌入代码
可以使用函数值✅ 是❌ 否(只能用字面量)

何时使用常量

  • 配置值(最大用户数、超时时间)
  • 数学常数(π, e)
  • 硬编码的字符串(应用名称、版本号)

5. 变量遮蔽 (Shadowing)

Rust 允许用相同的名称声明新变量 - 新变量会"遮蔽"旧变量:

#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1;  // 新 x 遮蔽了旧 x
println!("{}", x);  // 输出:6

{
    let x = x * 2;  // 在这个作用域内,x 是 12
    println!("内部作用域的 x: {}", x);
}

println!("外部作用域的 x: {}", x);  // 输出:6
}

遮蔽 vs 可变

// 使用 mut
let mut spaces = "   ";
spaces = spaces.len(); // ❌ 编译错误!类型不同

// 使用 shadowing
let spaces = "   ";
let spaces = spaces.len(); // ✅ 可以改变类型

遮蔽的优势

  • 可以改变类型
  • 可以复用名称(代码更简洁)
  • 在不同作用域有不同含义

常见错误

错误 1: 试图修改不可变变量

let x = 5;
x = 10; // ❌ 编译错误!

编译器输出:

error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:3:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     x = 10; // ❌ 编译错误!
  |     ^^^^^^^ cannot assign twice to immutable variable

修复方法

  1. 如果真的需要修改,添加 mut

    #![allow(unused)]
    fn main() {
    let mut x = 5;
    x = 10; // ✅ 现在可以了
    }
  2. 如果不需要修改,接受不可变性:

    #![allow(unused)]
    fn main() {
    let x = 5;
    let y = 10;  // 创建新变量而不是修改
    }

错误 2: 常量缺少类型注解

const MAX_SIZE = 100; // ❌ 编译错误!

编译器输出:

error[E0284]: type annotations needed
 --> src/main.rs:1:7
  |
1 | const MAX_SIZE = 100;
  |       ^^^^^^^^ consider giving `MAX_SIZE` a type

修复方法

添加类型注解:

#![allow(unused)]
fn main() {
const MAX_SIZE: i32 = 100; // ✅
}

错误 3: 未使用变量的警告

fn main() {
    let unused = 5; // ⚠️ 警告:未使用的变量
}

编译器输出:

warning: unused variable: `unused`
 --> src/main.rs:2:9
  |
2 |     let unused = 5;
  |         ^^^^^^ help: if this is intentional, prefix it with an underscore: `_unused`

修复方法

使用前缀下划线:

#![allow(unused)]
fn main() {
let _unused = 5; // ✅ 编译器知道你是故意的
}

动手练习

练习 1: 预测输出

不运行代码,预测下面代码的输出:

fn main() {
    let x = 5;
    let x = x + 1;
    {
        let x = x * 2;
        println!("内部:{}", x);
    }
    println!("外部:{}", x);
}
点击查看答案

输出:

内部:12
外部:6

解析

  1. x = 5 - 第一次绑定
  2. x = 5 + 1 = 6 - 遮蔽,新 x 是 6
  3. 内部作用域:x = 6 * 2 = 12 - 再次遮蔽
  4. 内部作用域结束,内部 x 失效
  5. 外部 x 仍然是 6

练习 2: 修复错误

下面的代码有编译错误,请修复:

fn main() {
    let counter = 0;
    counter = counter + 1;  // ❌ 错误
    println!("计数:{}", counter);
}
点击查看修复方法

修复

fn main() {
    let mut counter = 0;  // 添加 mut 关键字
    counter = counter + 1;  // ✅ 现在可以修改了
    println!("计数:{}", counter);
}

练习 3: 使用常量

改写下面的代码,使用常量代替硬编码的值:

fn main() {
    let tax = 0.08 * 100.0;  // 税率 8%
    println!("税费:{}", tax);
}
点击查看参考实现
fn main() {
    const TAX_RATE: f64 = 0.08;
    let amount = 100.0;
    let tax = TAX_RATE * amount;
    println!("税费:{}", tax);
}

好处

  • 税率集中定义
  • 易于修改
  • 避免魔术数字

故障排查 (FAQ)

Q: 什么时候应该使用 mut,什么时候不应该?

A: 遵循这个原则:

  • 默认不使用 mut - 99% 的情况不需要
  • 需要修改时使用 - 计数器、累加器、状态标志
  • 可以遮蔽时优先遮蔽 - 转换类型或复用名称

示例:

// ✅ 好的实践
let config = load_config();  // 不需要修改
let mut total = 0;  // 需要累加
for item in items {
    total += item.value;
}

// ❌ 不好的实践
let mut data = fetch_data();  // 如果不需要修改,为什么要 mut?
data = transform(data); // 考虑使用遮蔽:let data = transform(data);

Q: 常量为什么必须大写?

A: Rust 约定,不是强制要求。

  • 大写const MAX_VALUE - 约定俗成的常量命名
  • 小写let max_value - 变量命名

编译器不会强制,但违反约定会有警告。遵循约定让代码更易读。


Q: 遮蔽会让代码混乱吗?

A: 有时会。遵循这个指南:

好的遮蔽

#![allow(unused)]
fn main() {
let input = "  hello  ";
let input = input.trim();  // ✅ 明显是转换
}

不好的遮蔽

#![allow(unused)]
fn main() {
let x = 5;
let x = "string";  // ❌ 类型变化太大,让人困惑
let x = x.len();   // ❌ x 现在是什么?
}

规则:如果遮蔽让代码更难理解,使用不同的名称。


知识扩展 (选学)

表达式 vs 语句

Rust 中表达式和语句有重要区别:

语句 (Statement)

  • 执行动作,不返回值
  • 以分号 ; 结尾
  • 例如:let x = 5;

表达式 (Expression)

  • 计算并返回值
  • 不以分号结尾
  • 例如:x + 1

示例

#![allow(unused)]
fn main() {
let x = 5;  // 语句
let y = {   // 块表达式
    let z = 10;
    z + 1  // 表达式(没有分号!)
};
println!("y = {}", y); // 输出:11
}

表达式返回值

在 Rust 中,大多数结构都是表达式

// if 是表达式
let max = if a > b { a } else { b };

// match 是表达式
let description = match number {
    1 => "one",
    2 => "two",
    _ => "many",
};

// 循环也是表达式(返回 () 单元类型)
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2;  // 返回值
    }
};

这使得 Rust 代码更简洁、更具表现力。


小结

核心要点

  1. 变量默认不可变 - 这是 Rust 的安全特性
  2. 使用 mut 声明可变变量 - 只在需要时
  3. 常量用 const 定义 - 必须标注类型,全大写命名
  4. 遮蔽允许复用名称 - 可以改变类型,比 mut 更灵活
  5. Rust 大多数结构是表达式 - 返回值,不以分号结尾

关键术语

  • Binding (绑定): 将名称关联到值
  • Immutable (不可变): 不能修改的值
  • Mutable (可变): 可以修改的值
  • Shadowing (遮蔽): 用相同名称声明新变量
  • Expression (表达式): 返回值的代码
  • Statement (语句): 不返回值的代码动作

下一步


术语表

English中文
Variable变量
Constant常量
Immutable不可变
Mutable可变
Binding绑定
Expression表达式
Statement语句
Shadowing遮蔽
Type annotation类型注解

完整示例:src/basic/expression_sample.rs


延伸阅读

学习完变量与表达式后,你可能还想了解:

选择建议:

继续学习

💡 记住:不可变性是 Rust 的默认设置 - 如果你不特别告诉它"这个要改变",Rust 会让它保持不变。这是为了你的安全!


💡 小知识:为什么 Rust 变量默认不可变?

历史教训: 在 C/C++ 中,意外修改变量是常见 bug 来源:

// C 语言示例
int calculate(int x) {
    x = x * 2;  // 😱 意外修改了参数
    return x;
}

Rust 的设计哲学

"如果你需要修改,请明确说出来"

这叫做默认不可变 (immutable by default),好处是:

  1. 编译器能帮你发现错误 - 意外修改会报编译错误
  2. 更容易推理代码 - 知道值不会变
  3. 并发更安全 - 不可变数据可以在线程间安全共享

试试这个

// 猜猜哪行会报错?
let x = 5;
x = 6;  // ❌ 编译错误!

let mut y = 5;
y = 6;  // ✅ 可以修改

🌟 工业界应用:配置管理

场景:Web 服务器配置

struct ServerConfig {
    port: u16,        // 监听端口
    host: String,     // 绑定地址
    max_connections: u32,  // 最大连接数
}

fn main() {
    // 配置在初始化后不应该改变
    let config = ServerConfig {
        port: 8080,
        host: String::from("127.0.0.1"),
        max_connections: 1000,
    };
    
    // 使用配置(只读)
    println!("服务器启动在 {}:{}", config.host, config.port);
}

为什么不可变很重要

  • 防止运行中意外修改配置
  • 多个线程可以安全地读取同一配置
  • 编译器保证配置不会被篡改

🧪 动手试试:不可变的好处

练习:下面哪个场景应该用 mut

// A. 服务器端口配置
let port = 8080;

// B. 在线人数计数器
let user_count = 0;

// C. 数据库连接字符串
let db_url = "postgres://localhost/mydb";

// D. 购物车商品列表
let cart_items = Vec::new();
点击查看答案

答案

  • A. ❌ 不应该 mut - 端口配置不应该改变
  • B. ✅ 应该 mut - 计数器需要累加
  • C. ❌ 不应该 mut - 连接URL 不应该改变
  • D. ✅ 应该 mut - 购物车需要添加/删除商品

代码

#![allow(unused)]
fn main() {
let mut user_count = 0;
user_count += 1;  // ✅

let mut cart_items = Vec::new();
cart_items.push("商品");  // ✅
}

原则

默认不加 mut,需要修改时再加


知识检查

问题 1 🟢 (基础概念)

以下代码的输出是什么?

let x = 5;
let y = x + 3;
println!("{}", y);

A) 编译错误
B) 5
C) 8
D) 运行时错误

答案与解析

答案: C) 8

解析: i32 类型实现 Copy trait,赋值时复制值而非移动所有权。

问题 2 🟡 (移动语义)

这段代码有什么问题?

let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);
答案与解析

答案: 编译错误 - s1 的所有权已移动给 s2

修复: 使用引用 &s1 或克隆 s1.clone()

问题 3 🔴 (边界情况)

以下哪个表达式会在编译时被优化?

const X: i32 = 2 + 3;
let y = 4 * 5;
static Z: i32 = 6 + 7;
答案与解析

答案: const Xlet y 都会在编译时计算

解析: const 强制编译时求值,字面量表达式也会被编译器优化。static 在运行时初始化。

函数

开篇故事

想象你在组装乐高积木。每次需要搭建一个小房子时,你都要重新看说明书、找积木、一块块拼接——这既耗时又容易出错。但如果有一个"房子制作器"机器,你只需放入积木,按下按钮,房子就出来了!这就是函数的核心思想:将重复的逻辑封装起来,随时调用

在 Rust 中,函数是代码的基本构建块。通过函数,你可以将复杂的问题分解为小的、可管理的部分,让代码更易读、可复用。


本章适合谁

如果你已经学完了变量和表达式,现在想学习如何组织代码、避免重复,本章适合你。函数是编程的基础,无论你是什么水平的开发者,都会频繁使用函数。


你会学到什么

完成本章后,你可以:

  1. 使用 fn 关键字定义函数
  2. 理解参数和返回值的语法
  3. 区分表达式和语句
  4. 理解所有权的转移和借用
  5. 使用元组返回多个值

前置要求

学习本章前,你需要理解:


第一个例子

让我们看一个最简单的函数定义:

fn main() {
    let result = add(3, 5);
    println!("3 + 5 = {}", result);
}

fn add(a: i32, b: i32) -> i32 {
    a + b  // 隐式返回
}

发生了什么?

第 6 行定义了 add 函数:

  • fn: 函数声明关键字
  • add: 函数名称
  • (a: i32, b: i32): 两个参数,类型都是 i32
  • -> i32: 返回值类型
  • a + b: 函数体,没有分号表示返回值

第 2 行调用函数:add(3, 5) 返回 8


Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
函数定义def add(a, b):int add(int a, int b)int add(int a, int b)fn add(a: i32, b: i32) -> i32Rust 必须标注参数类型
返回值return a + breturn a + b;return a + b;a + b (无分号) 或 returnRust 支持隐式返回
多返回值return a, b需要类或数组需要结构体或引用(a, b) 元组Rust 用元组原生支持
所有权传递引用传递引用传递可选引用或值默认移动,用 & 借用Rust 编译时检查所有权
默认参数def f(a=1):不支持支持不支持Rust 用关联函数代替

核心差异: Rust 在函数签名上最严格,参数必须标注类型,但用元组优雅支持多返回值。


原理解析

1. 函数定义语法

fn function_name(parameter1: Type1, parameter2: Type2) -> ReturnType {
    // 函数体
    expression  // 返回值(无分号)
}

组成部分

  • fn: 声明函数的关键字
  • 函数名:使用 snake_case 命名(小写 + 下划线)
  • 参数:name: Type 格式,每个参数必须标注类型
  • 返回值:-> Type,如果无返回值可省略(相当于返回 ()
  • 函数体:花括号包围的代码块

2. 参数与所有权

fn main() {
    let s = String::from("hello");
    takes_ownership(s);
    // println!("{}", s); // ❌ 错误:s 已移动
    
    let x = 5;
    makes_copy(x);
    println!("{}", x); // ✅ 可以:i32 被复制
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
    // some_string 在这里被丢弃
}

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
    // some_integer 是 Copy trait,离开作用域不丢弃
}

关键点

  • 传递所有权给函数:参数获得值的所有权
  • Copy 类型(如 i32):自动复制,原变量仍可用
  • Drop 类型(如 String):所有权转移,原变量不可用

3. 返回值与隐式返回

// ✅ 隐式返回(推荐)
fn add(a: i32, b: i32) -> i32 {
    a + b  // 无分号
}

// ✅ 显式返回
fn add_explicit(a: i32, b: i32) -> i32 {
    return a + b;  // 有分号,使用 return 关键字
}

// ❌ 错误:有分号,返回 ()
fn add_wrong(a: i32, b: i32) -> i32 {
    a + b;  // 分号使这成为语句,返回 ()
}

规则

  • 最后一行表达式无分号 → 返回值
  • 使用 return 关键字 → 提前返回
  • 有分号的表达式 → 语句,不返回值

4. 使用元组返回多个值

fn main() {
    let (sum, product) = calculate(3, 5);
    println!("和:{}, 积:{}", sum, product);
}

fn calculate(a: i32, b: i32) -> (i32, i32) {
    let sum = a + b;
    let product = a * b;
    (sum, product)  // 返回元组
}

关键点

  • 返回类型:(Type1, Type2)
  • 返回值:用括号包围多个值
  • 解构:使用 let (a, b) = tuple 获取各个值

5. 函数参数模式

#![allow(unused)]
fn main() {
// 不可变参数(默认)
fn print_value(x: i32) {
    println!("{}", x);
    // x = x + 1; // ❌ 错误:不能修改
}

// 可变参数
fn modify_value(mut x: i32) {
    x = x + 1;  // ✅ 可以修改
    println!("{}", x);
}

// 借用参数(不获取所有权)
fn print_string(s: &String) {
    println!("{}", s);
    // s 仍归调用者所有
}

// 忽略参数
fn unused_param(_x: i32) {
    println!("不使用 x");
}
}

常见错误

错误 1: 忘记返回类型

// ❌ 错误:缺少返回类型
fn add(a: i32, b: i32) {
    a + b
}

// ✅ 正确
fn add(a: i32, b: i32) -> i32 {
    a + b
}

编译器输出:

error[E0308]: mismatched types
  --> src/main.rs:4:5
   |
1  | fn add(a: i32, b: i32) {
   |                       - help: add a return type: `-> i32`
...
4  |     a + b
   |     ^^^^^ expected `()`, found `i32`

错误 2: 返回值加分号

// ❌ 错误:返回值有分号
fn add(a: i32, b: i32) -> i32 {
    a + b;  // 分号使这成为语句
}

// ✅ 正确
fn add(a: i32, b: i32) -> i32 {
    a + b  // 无分号
}

编译器输出:

error[E0308]: mismatched types
 --> src/main.rs:3:5
  |
1 | fn add(a: i32, b: i32) -> i32 {
  |                             --- expected `i32` because of return type
2 |     a + b;
  |          - help: remove this semicolon to return this value
3 | }
  |     ^ expected `i32`, found `()`

错误 3: 所有权转移后使用

// ❌ 错误:所有权转移后使用变量
fn main() {
    let s = String::from("hello");
    print_string(s);
    println!("{}", s); // ❌ s 已移动
}

fn print_string(s: String) {
    println!("{}", s);
}

// ✅ 正确:使用借用
fn main() {
    let s = String::from("hello");
    print_string(&s);
    println!("{}", s); // ✅ s 仍可用
}

fn print_string(s: &String) {
    println!("{}", s);
}

错误 4: 参数类型不匹配

// ❌ 错误:类型不匹配
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    let result = add(3.0, 5.0); // f64 不是 i32
}

// ✅ 正确:使用正确类型
fn main() {
    let result = add(3, 5); // i32
}

动手练习

练习 1: 基础函数

定义一个函数 greet,接受名字参数并打印问候语:

// TODO: 定义 greet 函数

fn main() {
    greet("Alice");  // 应打印 "Hello, Alice!"
    greet("Bob");    // 应打印 "Hello, Bob!"
}
点击查看答案
fn greet(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    greet("Alice");
    greet("Bob");
}

解析: 使用 &str 作为参数类型,可以接受字符串字面量。

练习 2: 返回值

定义一个函数计算圆的面积:

// TODO: 定义 circle_area 函数,接受半径,返回面积

fn main() {
    let area = circle_area(5.0);
    println!("半径为 5 的圆面积:{}", area);
}
点击查看答案
fn circle_area(radius: f64) -> f64 {
    std::f64::consts::PI * radius * radius
}

fn main() {
    let area = circle_area(5.0);
    println!("半径为 5 的圆面积:{}", area);
}

解析: 使用 f64 处理浮点数,PI 常量在 std::f64::consts 中。

练习 3: 多返回值

定义一个函数同时返回商和余数:

// TODO: 定义 div_mod 函数,返回 (商,余数)

fn main() {
    let (quotient, remainder) = div_mod(17, 5);
    println!("17 / 5 = {} 余 {}", quotient, remainder);
}
点击查看答案
fn div_mod(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}

fn main() {
    let (quotient, remainder) = div_mod(17, 5);
    println!("17 / 5 = {} 余 {}", quotient, remainder);
}

解析: 使用元组 (i32, i32) 返回两个值,/ 是除法,% 是取余。

练习 4: 所有权与借用

完成以下代码,使 s 在调用后仍可用:

fn main() {
    let mut s = String::from("hello");
    
    // TODO: 修改 print_and_add 函数,使 s 在调用后仍可用
    print_and_add(&mut s);
    println!("修改后:{}", s);
}

fn print_and_add(s: String) {
    println!("{}", s);
    // s.push_str(" world"); // 如果取消注释,需要可变借用
}
点击查看答案
fn main() {
    let mut s = String::from("hello");
    print_and_add(&mut s);
    println!("修改后:{}", s);
}

fn print_and_add(s: &mut String) {
    println!("{}", s);
    s.push_str(" world");
}

解析: 使用 &mut String 可变借用,函数可以修改但不获取所有权。


故障排查

Q: 什么时候使用 return 关键字?

A: 通常不需要。Rust 函数隐式返回最后一个表达式。仅在以下情况使用:

  • 提前返回(在 if 语句中)
  • 从深层嵌套中返回
  • 使代码更清晰

Q: 参数类型应该用 &str 还是 &String

A: 优先使用 &str,因为:

  • 可以接受 &String 和字符串字面量
  • 更灵活、更通用
  • 除非需要 String 的方法,否则不需要 &String

Q: 函数可以返回引用吗?

A: 可以,但需要生命周期标注:

#![allow(unused)]
fn main() {
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
}

Q: 如何定义不接受参数的函数?

A: 使用空括号 ()

#![allow(unused)]
fn main() {
fn say_hello() {
    println!("Hello!");
}
}

知识扩展 (选学)

函数指针

函数也可以作为参数传递:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn calculate<F>(a: i32, b: i32, operation: F) -> i32
where
    F: Fn(i32, i32) -> i32,
{
    operation(a, b)
}

fn main() {
    let result = calculate(3, 5, add);
    println!("结果:{}", result);
}

泛型函数

函数可以接受泛型参数:

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

闭包简介

闭包是匿名函数,可以捕获环境变量:

#![allow(unused)]
fn main() {
let x = 5;
let add_x = |y| y + x;
println!("{}", add_x(3));  // 打印 8
}

小结

核心要点

  1. 函数定义: fn name(params) -> ReturnType { body }
  2. 参数类型: 必须标注类型,使用 name: Type 格式
  3. 返回值: 最后一个表达式(无分号)或使用 return
  4. 所有权: 参数默认获取所有权,使用 & 借用
  5. 元组返回: 使用 (a, b) 返回多个值

关键术语

  • Function: 函数,可重用的代码块
  • Parameter: 参数,函数的输入
  • Return Type: 返回类型,函数的输出类型
  • Ownership: 所有权,值的归属
  • Borrowing: 借用,临时使用值

下一步


术语表

English中文
Function函数
Parameter参数
Argument实参
Return返回
Ownership所有权
Borrow借用
Tuple元组

完整示例:src/basic/expression_sample.rs - 函数定义和调用
相关示例:src/basic/generic_sample.rs - 泛型函数示例


知识检查

快速测验(答案在下方):

  1. 这段代码能编译通过吗?为什么?
fn add(a: i32, b: i32) -> i32 {
    a + b;
}
  1. 函数参数默认是可变还是不可变?

  2. 如何返回多个值?

点击查看答案与解析
  1. ❌ 不能 - 返回值有分号,返回 () 而不是 i32
  2. 不可变 - 需要使用 mut 关键字
  3. 使用元组:fn foo() -> (i32, String) { (1, "hello".to_string()) }

关键理解: Rust 函数返回值是最后一个表达式(无分号)。

继续学习

💡 记住:函数是代码的基石。好的函数应该短小、专注、可复用!

基础数据类型

开篇故事

想象你在整理一个工具箱。你会把螺丝刀、锤子、尺子放在不同的格子里,因为每种工具用途不同。Rust 的数据类型就像这些格子——它们告诉编译器每个值应该如何存储和使用。


本章适合谁

如果你已经学完了变量与表达式,现在想了解 Rust 有哪些数据类型以及如何使用它们,本章适合你。数据类型是编程的基础,理解它们对编写正确的代码至关重要。


你会学到什么

完成本章后,你可以:

  1. 区分标量类型和复合类型
  2. 使用整数、浮点数、布尔值和字符类型
  3. 理解 String 和 &str 的区别
  4. 使用元组、数组、Vec 和 HashMap
  5. 进行类型转换和类型推断

前置要求


第一个例子

最简单的数据类型声明:

fn main() {
    // 整数类型
    let age: u8 = 25;
    
    // 浮点数类型
    let price: f64 = 19.99;
    
    // 布尔类型
    let is_active: bool = true;
    
    // 字符类型
    let initial: char = 'R';
    
    // 字符串类型
    let message: String = String::from("Hello, Rust!");
    
    println!("Age: {}, Price: {}, Active: {}, Initial: {}", 
             age, price, is_active, initial);
    println!("Message: {}", message);
}

完整示例: datatype_sample.rs


原理解析

标量类型 (Scalar Types)

Rust 有四种基本标量类型:

1. 整数类型

长度有符号无符号
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
架构相关isizeusize
#![allow(unused)]
fn main() {
let x: i32 = 42;      // 32 位有符号整数
let y: u64 = 1000;    // 64 位无符号整数
let z: isize = -5;    // 取决于架构
}

2. 浮点类型

#![allow(unused)]
fn main() {
let pi: f32 = 3.14;   // 32 位浮点数
let e: f64 = 2.718;   // 64 位浮点数 (默认)
}

3. 布尔类型

#![allow(unused)]
fn main() {
let is_true: bool = true;
let is_false: bool = false;
}

4. 字符类型

#![allow(unused)]
fn main() {
let letter: char = 'A';           // Unicode 字符
let emoji: char = '🦀';           // 支持 emoji
let chinese: char = '中';          // 支持中文
}

复合类型 (Compound Types)

1. 元组 (Tuple)

#![allow(unused)]
fn main() {
let tuple: (i32, f64, char) = (42, 3.14, 'A');

// 访问元组元素
let (x, y, z) = tuple;
println!("x: {}, y: {}, z: {}", x, y, z);

// 或使用索引
println!("First: {}", tuple.0);
}

2. 数组 (Array)

#![allow(unused)]
fn main() {
let array: [i32; 5] = [1, 2, 3, 4, 5];

// 访问数组元素
println!("First: {}", array[0]);
println!("Length: {}", array.len());

// 创建重复元素的数组
let repeated = [0; 5];  // [0, 0, 0, 0, 0]
}

3. 字符串 (String)

#![allow(unused)]
fn main() {
// String - 可增长的字符串
let mut greeting = String::from("Hello");
greeting.push_str(", world!");

// &str - 字符串切片
let slice: &str = &greeting[0..5];  // "Hello"
}

4. Vec (动态数组)

#![allow(unused)]
fn main() {
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);

// 或使用宏
let numbers = vec![1, 2, 3];
}

5. HashMap (哈希表)

#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert("Alice", 95);
scores.insert("Bob", 87);

// 访问
let alice_score = scores.get("Alice");
}

类型转换

1. 隐式转换 (类型推断)

#![allow(unused)]
fn main() {
let x = 42;  // 编译器推断为 i32
let y = 3.14; // 编译器推断为 f64
}

2. 显式转换

#![allow(unused)]
fn main() {
// 数值转换
let x: i32 = 42;
let y: f64 = x as f64;  // 42.0

// 字符串转换
let num_str = "42";
let num: i32 = num_str.parse().unwrap();
}

常见错误

错误 1: 整数溢出

let x: u8 = 255;
let y = x + 1;  // ❌ 编译错误:溢出

错误信息:

error: this arithmetic operation will overflow

修复方法:

#![allow(unused)]
fn main() {
// 使用更大的类型
let x: u16 = 255;
let y = x + 1;  // ✅ 256

// 或使用检查过的运算
let y = x.checked_add(1).unwrap();
}

错误 2: 类型不匹配

let x: i32 = 42;
let y: f64 = x;  // ❌ 编译错误:类型不匹配

修复方法:

let y: f64 = x as f64;  // ✅ 显式转换

错误 3: 数组越界

let array = [1, 2, 3];
let x = array[5];  // ❌ 运行时 panic

修复方法:

// 使用 get 方法安全访问
match array.get(5) {
    Some(value) => println!("{}", value),
    None => println!("Index out of bounds"),
}

动手练习

练习 1: 创建个人信息

创建一个程序,存储并打印个人信息:

fn main() {
    // TODO: 定义以下变量
    // - 姓名 (String)
    // - 年龄 (u8)
    // - 身高 (f32)
    // - 是否学生 (bool)
    
    // TODO: 打印所有信息
}
点击查看答案
fn main() {
    let name = String::from("张三");
    let age: u8 = 25;
    let height: f32 = 1.75;
    let is_student: bool = false;
    
    println!("姓名:{}, 年龄:{}, 身高:{}, 学生:{}", 
             name, age, height, is_student);
}

练习 2: 使用集合类型

创建一个程序,管理学生成绩:

use std::collections::HashMap;

fn main() {
    // TODO: 创建 HashMap 存储学生成绩
    // TODO: 添加 3 个学生
    // TODO: 计算平均分
}
点击查看答案
use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice", 95);
    scores.insert("Bob", 87);
    scores.insert("Charlie", 92);
    
    let total: i32 = scores.values().sum();
    let avg = total as f64 / scores.len() as f64;
    
    println!("平均分:{}", avg);
}

练习 3: 类型转换实践

fn main() {
    let num_str = "42";
    
    // TODO: 将字符串转换为 i32
    // TODO: 将 i32 转换为 f64
    // TODO: 打印结果
}
点击查看答案
fn main() {
    let num_str = "42";
    let num: i32 = num_str.parse().unwrap();
    let num_f64: f64 = num as f64;
    
    println!("i32: {}, f64: {}", num, num_f64);
}

故障排查 (FAQ)

Q: String 和 &str 有什么区别?

A:

  • String: 可增长、可变的字符串,存储在堆上
  • &str: 不可变的字符串切片,通常指向字符串字面量
#![allow(unused)]
fn main() {
let s1: String = String::from("hello");  // 可修改
let s2: &str = "world";                   // 不可修改
}

Q: 什么时候使用 Vec 而不是数组?

A:

  • 数组: 长度固定,类型 [T; N]
  • Vec: 长度可变,类型 Vec<T>
#![allow(unused)]
fn main() {
let array: [i32; 5] = [1, 2, 3, 4, 5];  // 固定长度
let vec: Vec<i32> = vec![1, 2, 3];       // 可变长度
}

Q: 如何选择整数类型?

A:

  • 一般情况:使用 i32 (最常用)
  • 需要大数:使用 i64i128
  • 索引/计数:使用 usize
  • 节省内存:使用 i8i16

知识扩展

日期与时间

Rust 标准库提供基础时间类型,第三方库 chrono 提供完整的日期时间处理:

1. 标准库时间类型

#![allow(unused)]
fn main() {
use std::time::{Instant, Duration, SystemTime};

// Instant - 用于测量时间间隔(如性能测试)
let start = Instant::now();
// ... 执行一些代码 ...
let elapsed = start.elapsed();
println!("耗时:{:?}", elapsed);

// Duration - 时间间隔
let one_second = Duration::from_secs(1);
let one_millisecond = Duration::from_millis(1);

// SystemTime - 系统时钟(可获取当前时间)
let now = SystemTime::now();
let since_epoch = now.duration_since(SystemTime::UNIX_EPOCH).unwrap();
println!("Unix 时间戳:{} 秒", since_epoch.as_secs());
}

2. chrono 库日期时间

use chrono::{Utc, Local, DateTime, NaiveDateTime};

// 获取当前 UTC 时间
let now_utc: DateTime<Utc> = Utc::now();
println!("UTC 时间:{}", now_utc);

// 获取本地时间
let now_local: DateTime<Local> = Local::now();
println!("本地时间:{}", now_local);

// 创建指定时间
let specific = Utc.with_ymd_and_hms(2024, 10, 26, 12, 30, 0).unwrap();
println!("指定时间:{}", specific);

// Unix 时间戳转换
let timestamp = 1700000000;
let datetime = DateTime::from_timestamp(timestamp, 0).unwrap();
println!("时间戳 {} 对应:{}", timestamp, datetime);

// 格式化输出
println!("{}", now_utc.format("%Y-%m-%d %H:%M:%S"));
println!("{}", now_utc.format("%Y年%m月%d日"));

// 解析字符串
let parsed = "2024-10-26 12:30:00"
    .parse::<NaiveDateTime>()
    .unwrap();
println!("解析结果:{}", parsed);

3. 时间类型对比

类型用途是否含时区使用场景
Instant测量时间间隔性能测试、超时检测
SystemTime系统时钟文件时间戳、日志时间
DateTime<Utc>UTC 时间是 (UTC)服务器时间、数据库存储
DateTime<Local>本地时间是 (本地)用户界面显示
NaiveDateTime无时区时间内部计算、解析中间值

4. Unix 时间戳

Unix 时间戳是从 1970 年 1 月 1 日 00:00:00 UTC 开始经过的秒数:

use chrono::{DateTime, Utc};

// DateTime → Unix 时间戳
let now = Utc::now();
let timestamp = now.timestamp();
println!("Unix 时间戳:{}", timestamp);

// Unix 时间戳 → DateTime
let datetime = DateTime::from_timestamp(timestamp, 0).unwrap();
println!("对应时间:{}", datetime);

BigDecimal 高精度计算

use bigdecimal::BigDecimal;

let a = BigDecimal::from(10);
let b = BigDecimal::from(3);
let result = &a / &b;

println!("{}", result);  // 3.333...

chrono 日期时间

use chrono::{Utc, DateTime};

let now: DateTime<Utc> = Utc::now();
println!("当前时间:{}", now);

// 格式化
println!("{}", now.format("%Y-%m-%d %H:%M:%S"));

集合类型对比

类型特点使用场景
数组固定长度,栈上已知大小的数据
Vec可变长度,堆上动态数据集合
HashMap键值对,哈希快速查找
BTreeMap键值对,有序有序数据

小结

核心要点:

  1. 标量类型: i32, f64, bool, char - 单个值
  2. 复合类型: 元组、数组、Vec、HashMap - 多个值
  3. String vs &str: 可变 vs 不可变
  4. 类型转换: 使用 as 进行显式转换
  5. 集合选择: 根据需求选择合适的数据结构

关键术语:

  • Scalar Type (标量类型): 单个值的类型
  • Compound Type (复合类型): 多个值的类型
  • Type Inference (类型推断): 编译器自动推断类型
  • Type Conversion (类型转换): 显式或隐式的类型转换

术语表

English中文
Scalar Type标量类型
Compound Type复合类型
Type Inference类型推断
Type Conversion类型转换
String Slice字符串切片
Vector向量
Hash Map哈希表

延伸阅读

学习完数据类型后,你可能还想了解:

选择建议:

继续学习

前一章: 变量与表达式
下一章: 了解所有权

相关章节:

返回: 基础入门


完整示例: datatype_sample.rs


📚 扩展阅读

完整示例代码:

相关章节:

外部资源:


🎓 知识检查题库

问题 1 🟢 (基础)

以下哪个是复合类型?

A) i32
B) bool
C) Vec
D) char

点击查看答案

答案: C) Vec

解析: Vec 是动态数组,可以存储多个值,属于复合类型。i32、bool、char 都是标量类型。

问题 2 🟡 (中等)

以下代码会编译通过吗?

let mut names = Vec::new();
names.push("Alice");
names.push(42);
点击查看答案

答案: 不会编译通过

解析: Vec 是类型安全的,所有元素必须是同一类型。这里试图混合 String 和 i32。

修复:

#![allow(unused)]
fn main() {
let mut names: Vec<&str> = Vec::new();
names.push("Alice");
// names.push(42);  // 需要改为字符串
}

问题 3 🔴 (困难)

HashMap 的底层实现是什么?

A) 红黑树
B) 哈希表
C) 链表
D) 堆

点击查看答案

答案: B) 哈希表

解析: HashMap 使用哈希表实现,提供 O(1) 平均时间复杂度的插入和查找。BTreeMap 使用红黑树。


✅ 本章完成检查点

  • 理解标量类型和复合类型的区别
  • 能够使用 String 和 &str
  • 能够创建和使用 Vec
  • 能够使用 HashMap 存储键值对
  • 能够进行类型转换
  • 完成 3 个动手练习
  • 通过知识检查题库

完整示例: datatype_sample.rs

了解所有权

开篇故事

想象你有一本珍贵的编程书。你可以把它借给朋友阅读,但同一时间只能有一个人拿着这本书。如果你的朋友正在读,你就不能同时读它。这就是 Rust 所有权的核心思想:每个值在任一时刻只能有一个所有者


本章适合谁

如果你已经学完了变量和数据类型,现在想理解 Rust 最独特的内存管理机制,本章适合你。所有权是 Rust 与其他语言最大的不同之处,需要多花些时间理解——这完全正常。


你会学到什么

完成本章后,你可以:

  1. 解释什么是所有权以及为什么 Rust 需要它
  2. 理解值何时被移动 (move) 以及移动的后果
  3. 识别所有权转移的代码模式
  4. 避免"移动后使用"的常见错误
  5. 理解如何正确返回函数内部创建的数据

前置要求

学习本章前,你需要理解:


第一个例子

让我们看一个最简单的所有权示例:

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s2); // ✅ 可以工作
// println!("{}", s1); // ❌ 编译错误!s1 已经移动给 s2 了
}

发生了什么?

第 2 行 let s2 = s1; 不是复制字符串,而是转移所有权s1 的所有权移动给了 s2s1 不再有效。

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
变量赋值s2 = s1 (引用)s2 = s1 (引用)s2 = s1 (浅拷贝)let s2 = s1; (移动)Rust 转移所有权,其他语言共享
内存管理垃圾回收 (GC)垃圾回收 (GC)手动 delete / 智能指针所有权系统 (编译时检查)Rust 无运行时 GC
字符串复制s2 = s1[:] (显式)s2 = s1.clone()s2 = s1 (浅) / 深拷贝let s2 = s1.clone();Rust 显式克隆,默认移动
函数参数传递引用 (默认)传递引用 (对象)值传递 / 引用传递移动 (默认) / 借用 (&)Rust 默认转移所有权
悬垂指针不可能 (GC 保护)不可能 (GC 保护)可能 (运行时错误)编译时阻止Rust 在编译时防止

原理解析

数据支撑:为什么所有权很重要?

工业界数据:

  • Microsoft 2019 报告: 约 70% 的 CVE(安全漏洞)是内存安全问题
  • Google Chrome 团队: 约 70% 的高严重性 bug 是内存安全问题
  • Mozilla Firefox: 在引入 Rust 后,内存安全漏洞减少了 90%+
  • Linux 内核: 从 2022 年开始接受 Rust 代码,主要目标就是消除内存安全漏洞

内存数据 (64 位系统):

  • String 在栈上占 24 字节(指针 8 字节 + 长度 8 字节 + 容量 8 字节)
  • 实际字符串数据在堆上,占用 N 字节
  • 移动操作只复制 24 字节,非常快(约 1-2 纳秒)

初学者常见困惑

💡 这是很多学习者第一次遇到所有权时的困惑——你并不孤单!

困惑 1: "为什么 Rust 要设计这么复杂的所有权系统?其他语言都没有啊!"

解答: 其他语言用垃圾回收 (GC)手动内存管理来解决问题,但都有代价:

  • GC 语言 (Java, Python, Go): 运行时有 GC 开销(约 5-15% 性能损失),且 GC 暂停不可预测
  • 手动管理 (C, C++): 程序员负责,容易出错(Microsoft 报告 70% 漏洞源于此)
  • Rust 所有权: 编译时检查,零运行时开销,同时保证内存安全

困惑 2: "移动后原变量就不能用了,这太不方便了吧?"

解答: 这正是 Rust 的设计哲学——安全优先于方便。但 Rust 提供了灵活的解决方案:

  • 只需读取 → 用借用 &(零开销)
  • 需要独立副本 → 用 .clone()(有开销,但明确)
  • 函数需要返回 → 直接返回(移动所有权回去)

真实案例: Dropbox 在将核心同步引擎从 C++ 重写为 Rust 后,CPU 使用减少了 40%,显著提高了可靠性和稳定性。所有权系统是这一改进的核心。

所有权内存模型

栈 (Stack)                    堆 (Heap)
+---------------+          +----------------+
| s1 (pointer)  |--------->| "hello"        |
| length: 5     |          |                |
| capacity: 5   |          +----------------+
+---------------+

let s2 = s1;  // 移动后:

栈 (Stack)                    堆 (Heap)
+---------------+          +----------------+
| s1 (无效)     |          | "hello"        |
| s2 (pointer)  |--------->|                |
| length: 5     |          |                |
| capacity: 5   |          +----------------+
+---------------+

关键点

  • 移动后 s1 变为无效,编译器防止使用
  • 堆内存不会被释放两次(防止 double free)
  • 只有 s2 可以访问和释放堆内存

借用 vs 移动

移动 (Move):
s1 ──→ [堆内存]     s2 = s1     s1 ❌  s2 ──→ [堆内存]

借用 (Borrow):
s1 ──→ [堆内存]     r = &s1     s1 ──→ [堆内存] ←── r ✅

所有权三规则

Rust 的所有权系统遵循三条简单规则:

  1. 每个值都有一个所有者

    • 变量是值的所有者
    • 所有者负责清理值
  2. 任一时刻只能有一个所有者

    • 不像其他语言可以有多个引用指向同一数据
    • Rust 确保内存安全
  3. 所有者离开作用域,值被丢弃

    • 自动清理,无需手动释放
    • 防止内存泄漏

移动语义 (Move Semantics)

当你在 Rust 中赋值或传递值时:

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1;  // s1 的所有权移动给 s2
}

对于栈上的数据 (如 i32, bool):

  • 直接复制(非常快)
  • 原变量仍然可用

对于堆上的数据 (如 String, Vec):

  • 只复制指针、长度、容量
  • 原变量不再有效
  • 保证只有一个变量负责清理堆内存

常见错误

错误 1: 移动后使用 (Use After Move)

❌ 错误代码

let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // ❌ 编译错误!

🤔 为什么这行不编译?

编译器会告诉你:

error[E0382]: borrow of moved value: `s1`
  |
2 |     let s2 = s1;
  |              -- value moved here
3 |     println!("{}", s1); // ❌ 编译错误!
  |                    ^^ value borrowed here after move

解释s1 在第 2 行已经移动给 s2 了。Rust 不允许使用已移动的变量,这是为了防止"悬垂指针"——如果 s2 清理了堆内存,s1 就会指向无效数据!

✅ 修复方法 1:如果只需读取,使用引用(借用):

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = &s1;  // 借用,不移动
println!("{}", s1); // ✅ s1 仍然可用
println!("{}", s2); // ✅ s2 是引用
}

✅ 修复方法 2:如果需要两个独立的 String,使用克隆:

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();  // 深度复制
println!("{}", s1); // ✅ 两者都可用
println!("{}", s2); // ✅
}

错误 2: 函数参数移动

fn takes_ownership(s: String) {
    println!("Got: {}", s);
} // s 离开作用域,被丢弃

let my_string = String::from("hello");
takes_ownership(my_string);
println!("{}", my_string); // ❌ 编译错误!my_string 已经移动给函数了

修复方法

  1. 使用引用传递(推荐):

    #![allow(unused)]
    fn main() {
    fn borrows_string(s: &String) {
        println!("Borrowed: {}", s);
    } // 不获取所有权,只是借用
    
    let my_string = String::from("hello");
    borrows_string(&my_string);
    println!("{}", my_string); // ✅ my_string 仍然可用
    }
  2. 返回所有权

    fn takes_and_returns(s: String) -> String {
        println!("Got: {}", s);
        s // 返回所有权
    }
    
    let my_string = String::from("hello");
    my_string = takes_and_returns(my_string);
    println!("{}", my_string); // ✅ 所有权回来了

错误 3: 试图返回局部变量的引用

fn returns_local_ref() -> &String {
    let s = String::from("hello");
    &s // ❌ 编译错误!s 会在函数结束时被清理
}

编译器输出:

error[E0515]: cannot return reference to local variable `s`
 --> src/main.rs:4:5
  |
4 |     &s // ❌ 编译错误!s 会在函数结束时被清理
  |     ^^ returns a reference to data owned by the current function

为什么错误s 在函数栈上创建,函数结束时会被清理。返回指向它的引用会导致悬垂指针

修复方法

  1. 直接返回值(转移所有权):

    #![allow(unused)]
    fn main() {
    fn returns_owned_string() -> String {
        let s = String::from("hello");
        s // ✅ 移动所有权给调用者
    }
    }
  2. 使用静态字符串:

    #![allow(unused)]
    fn main() {
    fn returns_static() -> &'static str {
        "hello" // ✅ 字符串字面量有 'static 生命周期
    }
    }
  3. 使用生命周期标注(高级主题,后续章节详述):

    #![allow(unused)]
    fn main() {
    fn get_ref<'a>(data: &'a str) -> &'a str {
        data // ✅ 返回外部的引用
    }
    }

动手练习

🟢 入门练习:识别所有权转移

下面的代码会编译通过吗?如果不会,如何修复?

💡 编译器是你的老师:尝试运行这段代码,仔细阅读编译器错误信息。它会告诉你移动发生在哪里!

fn main() {
    let x = String::from("hello");
    let y = x;
    let z = x; // 这里会发生什么?
    println!("{}", x);
}
点击查看答案与解析

答案:❌ 不会编译通过

解析: 第 3 行 let z = x; 试图使用已经移动给 yx。第 4 行再次使用 x,编译错误。

修复方案

#![allow(unused)]
fn main() {
let x = String::from("hello");
let y = x.clone(); // 克隆,不移动
let z = x.clone(); // 再次克隆
println!("{}", x); // ✅ 三者都可用
}

🟡 中级练习:修复函数参数

补全下面的函数,使得调用后 original 仍然可用:

💡 提示:想想"借用"和"所有权"的区别。如果你只需要读取,不需要拥有,应该用什么?

fn print_and_??? (s: ???) {
    println!("Length: {}", s.len());
}

fn main() {
    let original = String::from("hello");
    print_and_???(&???);
    println!("Still have: {}", original); // 应该可以工作
}
点击查看答案

答案

fn print_and_keep(s: &String) {  // 或者 &str
    println!("Length: {}", s.len());
}

fn main() {
    let original = String::from("hello");
    print_and_keep(&original);  // 传递引用
    println!("Still have: {}", original); ✅
}

🔴 挑战练习:理解移动与复制

预测下面代码哪些会编译通过:

💡 挑战:先不看答案,自己推理每个案例。思考"这个类型实现了 Copy trait 吗?"

// A
let a = 5;
let b = a;
println!("{}", a);

// B
let s1 = String::from("test");
let s2 = s1;
println!("{}", s1);

// C
let s1 = 42;
let s2 = s1;
let s3 = s1;
println!("{}", s1);
点击查看解析

A: ✅ 通过 - i32 类型在栈上,直接复制 B: ❌ 失败 - String 转移所有权,s1 不再可用 C: ✅ 通过 - i32 类型,可以多次复制使用

关键点Copy trait 类型(如所有整数)会自动复制,其他类型(如 String)会转移所有权。


故障排查 (FAQ)

Q: 为什么 Rust 要设计这么复杂的所有权系统?

A: 为了内存安全零成本抽象

  • 内存安全:不使用垃圾回收,也能防止悬垂指针、双重释放等问题
  • 零成本:编译时检查,运行时无额外开销
  • 并发安全:所有权规则天然防止数据竞争

虽然学习曲线陡峭,但掌握后你会写出更可靠的代码。


Q: 每次都要 clone() 会不会很慢?

A: 确实有性能开销,但:

  1. 优先使用引用 - 大部分情况不需要克隆
  2. 只在必要时克隆 - 当确实需要两份独立数据时
  3. 使用 Rc/Arc - 需要共享所有权时的智能指针

性能敏感的代码可以进行基准测试,但先保证正确性。


Q: 如何调试"值已移动"的错误?

A: 遵循这个流程:

  1. 编译器会告诉你移动发生在哪里

    value moved here
    
  2. 问自己

    • 我真的需要所有权吗?还是只需要读取?→ 使用引用
    • 我需要两份独立的数据吗?→ 使用 clone()
    • 可以多线程共享吗?→ 使用 Arc
  3. 画出所有权流程图

    s1 --移动--> s2 --移动--> s3
    

Q: 所有权和 borrow 有什么区别?

A:

所有权 (Ownership)借用 (Borrowing)
独占访问可以共享访问
负责清理不负责清理
s1 = s2 (移动)&s1 (引用)
只能有一个所有者可以有多个借用
可以修改和读取取决于可变/不可变借用

知识扩展 (选学)

Copy trait

有些类型实现了 Copy trait,赋值时自动复制而不是移动:

#![allow(unused)]
fn main() {
let x = 5;  // i32 实现了 Copy
let y = x;  // 复制值,x 仍然可用
println!("{}", x); // ✅ 可以
}

哪些类型有 Copy trait

  • 所有整数类型 (i32, u64, etc.)
  • 布尔值 (bool)
  • 浮点数 (f64, f32)
  • 字符 (char)
  • 元组(如果所有成员都有 Copy)
  • 指针 (&T)

哪些类型没有 Copy

  • String
  • Vec<T>
  • 任何包含上述类型的结构体

如果你想让自己的类型有 Copy 行为,实现 Copy trait (所有成员必须是 Copy 类型)。


Drop trait

当值离开作用域时,Rust 会自动调用 drop 方法清理资源:

struct MyFile {
    path: String,
}

impl Drop for MyFile {
    fn drop(&mut self) {
        println!("Cleaning up file: {}", self.path);
    }
}

fn main() {
    let f = MyFile { path: String::from("/tmp/test.txt") };
    println!("File created");
} // f 离开作用域,调用 drop 方法,打印 "Cleaning up file..."

你不需要手动调用 drop(实际上也不鼓励),Rust 会自动处理。


小结

核心要点

  1. 所有权是 Rust 管理内存的方式,每个值有且只有一个所有者
  2. 赋值 = 移动(对于非 Copy 类型),原变量不再可用
  3. 函数参数默认转移所有权,使用引用避免移动
  4. 不能返回局部变量的引用,会创建悬垂指针
  5. 使用 clone() 在需要独立副本时,使用 引用 在只需读取时

关键术语

  • 所有权 (Ownership): 对值的独占访问和清理责任
  • 移动 (Move): 所有权的转移
  • Copy trait: 自动复制的类型
  • 借用 (Borrow): 临时访问,不转移所有权

🧠 学习提示

所有权是 Rust 最独特的特性,也是很多初学者的第一道坎。如果你感到困惑,这完全正常!

Microsoft Rust 培训建议:"Struggling with the borrow checker is part of learning. If stuck >15 minutes → check solution, study, close, try again from scratch."

推荐学习流程

  1. 先自己写代码,让编译器报错
  2. 仔细阅读错误信息(Rust 的编译器是最好的老师)
  3. 如果卡住超过 15 分钟,查看答案
  4. 关掉答案,从头自己写一遍

下一步

  • 学习 借用和引用 - 如何在不转移所有权的情况下使用值
  • 理解 生命周期 - 确保引用不会超出有效范围

术语表

English中文
Ownership所有权
Move移动
Borrow借用
Copy traitCopy trait
Dangling pointer悬垂指针
Drop丢弃/释放

完整示例:src/basic/ownership_sample.rs


延伸阅读

学习完所有权后,你可能还想了解:

选择建议:

继续学习

💡 记住:所有权是 Rust 最独特的特性,也是很多初学者的第一道坎。如果你感到困惑,这完全正常。多写代码,多看编译器错误,你会逐渐掌握它!

解决方法:

要将函数内部创建的数据“返回”出来,你必须转移该数据的所有权。Rust 的移动语义(Move Semantics)使得这变得简单且安全:

  • 直接返回数据 (按值返回): 函数返回类型是 T,你直接返回函数内部创建的变量。数据的所有权从函数内部转移到调用者。
#![allow(unused)]
fn main() {
fn create_value_and_return_owned() -> i32 {
    let value = 42; // value 在函数栈上
    value // value 的所有权被移出函数
} // value 在这里不会被 drop,因为它已经被移出

fn create_string_and_return_owned() -> String {
    let text = String::from("hello"); // text 在函数栈上,但其数据在堆上
    text // text 的所有权被移出函数。堆上的数据不会被清理。
} // text 在这里不会被 drop

fn create_box_and_return_owned() -> Box<i32> {
    let boxed_value = Box::new(100); // boxed_value 在函数栈上,它指向堆上的数据
    boxed_value // boxed_value 的所有权被移出函数。堆上的数据不会被清理。
} // boxed_value 在这里不会被 drop

}
  • 返回智能指针: 如果你需要共享数据,可以将内部创建的数据包装在 Rc 或 Arc 等智能指针中,并返回智能指针的副本。数据的实际所有权由智能指针管理,而你返回的是智能指针的共享引用或智能指针本身(所有权转移)。
use std::sync::Arc;
use std::cell::RefCell;

fn create_shared_data() -> Arc<RefCell<i32>> {
    let data = RefCell::new(0); // data 在函数栈上,它包装了堆上的数据
    Arc::new(data) // Arc::new 会将 RefCell 移动到堆上,并返回 Arc 的所有权
} // data 在这里不会被 drop,因为它内部的 RefCell 已经被移到堆上并被 Arc 拥有

fn ownership_shared_sample() {
    let shared = create_shared_data(); // shared 现在拥有 Arc 的所有权
    println!("{}", shared.borrow());
}

///
/// 单元测试
/// #[cfg(test)]
///
#[cfg(test)]
mod tests {
    // 注意这个惯用法:在 tests 模块中,从外部作用域导入所有名字。
    use super::*;

    #[test]
    fn test_ownership_shared_sample() {
        ownership_shared_sample();
        println!("print test in mdbook")
    }
}

💡 小知识:所有权的历史渊源

问题来源: 在 C++ 中,内存管理是程序员的责任:

// C++ 示例
void process() {
    std::string* s = new std::string("hello");
    // ... 使用 s ...
    delete s;  // 😰 忘记了就会内存泄漏
}

// 或者更糟:
std::string* get_string() {
    std::string s = "hello";
    return &s;  // ❌ 返回局部变量的指针!
}

Rust 的创新

"让编译器在编译时检查内存安全,而不是在运行时"

所有权系统的核心思想:

  1. 每个值有一个所有者 - 明确责任
  2. 所有者离开作用域,值被清理 - 自动内存管理
  3. 借用检查 - 防止悬垂指针

对比其他语言

语言内存管理方式优点缺点
C/C++手动管理完全控制容易泄漏、悬垂指针
Java垃圾回收 (GC)简单运行时开销、停顿
Rust所有权系统零开销、编译时检查学习曲线陡峭

🌟 工业界应用:防止内存泄漏

真实案例: 某金融公司用 C++ 写交易系统,遇到内存泄漏:

// 模拟场景
void process_trade() {
    Trade* trade = new Trade();
    if (validate(trade)) {
        execute(trade);
        // 😱 忘记 delete,每次交易泄漏内存
    }
    // 一天后,系统内存耗尽崩溃
}

Rust 方案

fn process_trade() {
    let trade = Trade::new();
    if validate(&trade) {
        execute(&trade);
    }
    // ✅ trade 自动清理,无需手动 delete
}

结果

  • 内存泄漏:归零
  • 性能:提升 40% (无 GC 开销)
  • 开发效率:提高 2x (不用调试内存问题)

🧪 动手试试:所有权规则

练习:预测每段代码的输出

// A. 移动语义
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s2);  // 输出:?
// println!("{}", s1);  // 会发生什么?

// B. 克隆
let s3 = String::from("world");
let s4 = s3.clone();
println!("{}, {}", s3, s4);  // 输出:?

// C. 借用
let s5 = String::from("rust");
let s6 = &s5;
println!("{}, {}", s5, s6);  // 输出:?
点击查看答案与解析

答案

A.

输出:hello
s1 那行会编译错误 - s1 已经移动给 s2

B.

输出:world, world
clone() 创建独立副本,两者都可用

C.

输出:rust, rust
借用不转移所有权,两者都可用

关键区别

  • 移动 (let s2 = s1) - s1 不能再使用
  • 克隆 (s3.clone()) - 创建独立副本
  • 借用 (&s5) - 临时访问,不影响所有者

内存布局可视化

1. 栈 vs 堆内存

栈内存 (Stack)          堆内存 (Heap)
+---------------+       
| pointer       |------+-----> +-------------------+
| length: 5     |       |      | "hello"           |
| capacity: 5   |       |      |                   |
+---------------+       |      +-------------------+
                        |
s1 变量 (所有者) --------+

说明:

  • 栈上存储:指针、长度、容量 (24 字节)
  • 堆上存储:实际字符串数据 ("hello")
  • s1 是指针,指向堆上数据的所有者

2. 所有权转移

转移前:
s1 ──────────→ [堆内存:"hello"]
  (所有者)

执行 s2 = s1 后:
s1             [堆内存:"hello"] ←────────── s2
  ❌ 无效        ↑                        ✅ 新所有者
                所有权转移

关键点:

  • 移动后原变量失效
  • 所有权只有一个
  • 转移是浅拷贝(只复制指针,不复制数据)

3. 借用规则

不可变借用 (多个允许):
&s1 ──→ [data] ←── &s2
&s3 ──→              ↑
                      所有者

可变借用 (独占访问):
&mut s1 ──→ [data]   ←── 所有者
              ↑
         (其他引用不允许)

规则:

  • 多个不可变借用 ✓
  • 一个可变借用 ✓
  • 同时有可变和不可变借用 ✗

知识检查

问题 1 🟢 (基础概念)

以下代码的输出是什么?

let s1 = String::from("hello");
let s2 = s1;
println!("{}", s2);

A) 编译错误
B) "hello"
C) 运行时错误
D) 空字符串

答案与解析

答案: B) "hello"

解析: s2 获得所有权后,可以正常使用。s1 不能再使用,但 s2 可以。

问题 2 🟡 (借用规则)

这段代码违反了什么规则?

let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;  // ❌
答案与解析

答案: 违反借用规则 - 不可变借用存在时不能有可变借用

修复: 确保可变借用前,所有不可变借用已停止使用

问题 3 🔴 (生命周期)

这个函数的签名应该如何修正?

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
答案与解析

答案: 需要显式生命周期标注

修复:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
}

生命周期

开篇故事

想象你在图书馆借书。每本书都有一个借阅期限标签,告诉你这本书必须在什么时候归还。如果借阅期限到了,你就不能再使用这本书。Rust 的生命周期就像是这些借阅期限标签——它们告诉编译器引用的有效范围,确保你永远不会使用一个已经"过期"的引用。

生命周期是 Rust 最独特的特性之一。它确保内存安全,无需垃圾回收。理解生命周期是掌握 Rust 借用检查器的关键。


本章适合谁

如果你已经理解了所有权和借用,但在编译时遇到类似"borrowed value does not live long enough"的错误,本章适合你。生命周期标注听起来复杂,但一旦理解,就能轻松解决这类错误。


你会学到什么

完成本章后,你可以:

  1. 理解生命周期的概念和作用
  2. 识别需要生命周期标注的场景
  3. 使用 'a 语法标注生命周期
  4. 理解生命周期省略规则
  5. 解决常见的生命周期错误

前置要求

学习本章前,你需要理解:


第一个例子

让我们看一个最简单的生命周期示例:

fn main() {
    let r;                // ---------+-- 'a (外层作用域)
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

发生了什么?

变量 x 的生命周期是 'b(内层花括号),引用 r 的生命周期是 'a(整个 main 函数)。

第 5 行 r = &x; 试图将短生命周期的引用赋给长生命周期的变量,这会导致悬垂引用错误。

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你理解为什么 Rust 需要生命周期:

概念PythonJavaC++Rust关键差异
引用有效性运行时检查 (GC)运行时检查 (GC)不检查 (可能悬垂)编译时检查 (生命周期)Rust 在编译时防止悬垂引用
返回局部变量引用不可能 (GC 保护)不可能 (GC 保护)可能 (运行时崩溃)编译时阻止Rust 编译时保证安全
函数返回引用总是安全总是安全需要小心需要标注生命周期Rust 显式声明引用关系
内存安全保证运行时 GC运行时 GC程序员负责编译时所有权 + 生命周期Rust 零运行时开销

编译器输出:

error[E0597]: `x` does not live long enough
 --> src/main.rs:5:13
  |
5 |         r = &x;
  |             ^^ borrowed value does not live long enough
6 |     }
  |     - `x` dropped here while still borrowed

💡 编译器是你的老师:注意编译器用 ASCII 图告诉你问题所在!^ 标记了出错的引用,- 标记了变量被丢弃的位置。学会阅读这些图,它们是你最好的学习工具!


原理解析

数据支撑:为什么生命周期很重要?

工业界数据:

  • 悬垂指针 bug: 在 C/C++ 中,约 30% 的内存安全漏洞源于悬垂指针(返回已释放内存的引用)
  • Rust 编译时检查: 生命周期标注在编译时 100% 消除悬垂指针,零运行时开销
  • GC 语言对比: Java/Python 用 GC 防止悬垂指针,但 GC 暂停可达 100ms+(不可预测)
  • Rust 优势: 生命周期检查在编译时完成(约增加 0.1-0.3 秒 编译时间),运行时无任何开销

真实案例:

  • Firefox Quantum: 使用 Rust 重写 CSS 引擎(Servo),生命周期系统确保无悬垂引用,性能提升 2 倍
  • AWS Firecracker: 微虚拟机使用 Rust,生命周期保证内存安全,大幅减少内存安全漏洞
  • Discord: 从 Go 迁移到 Rust,消除了 GC 暂停,延迟从 P99 500ms 降到 P99 5ms

初学者常见困惑

💡 这是很多学习者第一次遇到生命周期时的困惑——你并不孤单!

困惑 1: "生命周期标注看起来很复杂,为什么不能像 Java 那样自动处理?"

解答: Java 用 GC(垃圾回收) 自动处理,但代价是:

  • 运行时开销: GC 需要定期暂停程序扫描内存(5-15% 性能损失)
  • 不可预测: GC 暂停时间不确定,不适合实时系统
  • 内存占用: GC 需要额外的内存来追踪对象

Rust 用生命周期标注在编译时解决,代价是:

  • 学习成本: 需要理解生命周期概念(约 2-3 小时)
  • 编译时检查: 编译时间略微增加(可忽略)
  • 运行时无开销: 零性能损失

困惑 2: "'a 到底是什么?为什么看起来像个奇怪的标签?"

解答: 'a 只是一个名字,就像给变量取名一样。编译器用它来追踪引用的有效范围:

'library_hours = 9:00-21:00    ← 图书馆开放时间
'your_visit = 10:00-11:00      ← 你的访问时间

'your_visit 必须在 'library_hours 内 → 'library_hours: 'your_visit

在 Rust 中:

'a = 外层作用域(比如整个 main 函数)
'b = 内层作用域(比如一个 if 块)

'b 必须在 'a 内 → 'a: 'b

困惑 3: "为什么有时候不需要标注生命周期?"

解答: Rust 有生命周期省略规则——编译器能自动推断时不需要标注。约 80% 的函数不需要手动标注。只有当编译器无法确定时才需要手动标注。

生命周期关系图

'a: [=======================================]  (外层作用域)
      'b: [==================]               (内层作用域)
      
'a 包含 'b → 'b: 'a (b 的生命周期是 a 的子集)

函数签名:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
         │         │          │          │
         │         │          │          └── 返回值生命周期 = 较短的那个
         │         │          └── y 活至少 'a
         │         └── x 活至少 'a
         └── 声明生命周期参数

1. 什么是生命周期?

生命周期是引用的有效作用域。每个引用都有生命周期,编译器自动推断,通常无需标注。

#![allow(unused)]
fn main() {
{
    let x = 5;            // 'a 开始
    let r = &x;           // r 的生命周期依赖于 x
    println!("{}", r);
}                         // 'a 结束,x 和 r 都失效
}

关键点

  • 生命周期是作用域的名称
  • 编译器确保引用不会比它指向的数据活得更久
  • 防止悬垂引用(dangling reference)

2. 生命周期标注语法

当编译器无法自动推断时,需要手动标注:

#![allow(unused)]
fn main() {
// 泛型生命周期参数 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
}

语法解析

  • <'a>: 声明生命周期参数
  • &'a str: 引用活至少 'a 这么久
  • -> &'a str: 返回值也活至少 'a 这么久

含义:返回值的生命周期与两个参数中较短的那个相同。

3. 结构体中的生命周期

当结构体持有引用时,必须标注生命周期:

// ❌ 错误:缺少生命周期
struct Excerpt {
    part: &str,
}

// ✅ 正确:标注生命周期
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    
    let excerpt = Excerpt {
        part: first_sentence,
    };
    
    println!("摘录:{}", excerpt.part);
}

规则

  • 结构体有引用字段 → 需要生命周期参数
  • 生命周期参数放在结构体名后:struct Name<'a>
  • 每个引用字段使用该生命周期:field: &'a Type

4. 生命周期省略规则

编译器使用三条规则自动推断生命周期,无需标注:

规则 1: 每个引用参数获得独立的生命周期

fn first_word(s: &str) -> &str {
    // 实际被推断为:
    // fn first_word<'a>(s: &'a str) -> &'a str
}

规则 2: 如果只有一个输入生命周期,它被赋给所有输出生命周期

fn longest(x: &str, y: &str) -> &str {
    // 无法推断,因为有两个输入生命周期
    // 必须手动标注
}

规则 3: 如果有 &self&mut selfself 的生命周期被赋给所有输出生命周期

impl<'a> Excerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
    
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        // self 的生命周期赋给返回值
        println!("Attention please: {}", announcement);
        self.part
    }
}

5. 'static 生命周期

'static 是特殊的生命周期,表示整个程序运行期间都有效

#![allow(unused)]
fn main() {
// 字符串字面量是 &'static str
let s: &'static str = "I live forever!";

// 函数返回 'static
fn get_static() -> &'static str {
    "also lives forever"
}
}

常见场景

  • 字符串字面量
  • 全局变量
  • 单例

常见错误

错误 1: 悬垂引用

// ❌ 错误:返回局部变量的引用
fn dangling_reference() -> &i32 {
    let x = 5;
    &x  // x 在函数结束时被丢弃
}

// ✅ 正确:返回拥有的值
fn no_dangling() -> i32 {
    let x = 5;
    x  // 返回值,不是引用
}

编译器输出:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:31
  |
1 | fn dangling_reference() -> &i32 {
  |                               ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

错误 2: 生命周期不匹配

// ❌ 错误:返回值生命周期不明确
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// ✅ 正确:标注生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

错误 3: 结构体缺少生命周期

// ❌ 错误
struct Excerpt {
    part: &str,
}

// ✅ 正确
struct Excerpt<'a> {
    part: &'a str,
}

编译器输出:

error[E0106]: missing lifetime specifier
 --> src/main.rs:2:11
  |
2 |     part: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Excerpt<'a> {
2 ~     part: &'a str,

错误 4: 过度标注生命周期

// ❌ 不必要:编译器可以推断
fn print(s: &str) {
    println!("{}", s);
}

// ✅ 更好:让编译器处理
fn print(s: &str) {
    println!("{}", s);
}

规则:只在编译器要求时标注生命周期。


动手练习

🟢 入门练习:添加生命周期标注

为以下函数添加生命周期标注使其编译通过:

💡 编译器是你的老师:先不标注生命周期,让编译器报错。仔细阅读错误信息,它会告诉你需要添加什么!

// TODO: 添加生命周期参数
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = String::from("long string is long");
    let s2 = String::from("xyz");
    let result = longest(&s1, &s2);
    println!("最长的字符串是:{}", result);
}
点击查看答案
#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
}

解析: 返回值依赖于两个参数,需要明确标注生命周期 'a

练习 2: 结构体生命周期

定义一个持有字符串切片引用的结构体:

// TODO: 定义 Excerpt 结构体,包含 part: &str 字段

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    
    let excerpt = Excerpt {
        part: first_sentence,
    };
    
    println!("摘录:{}", excerpt.part);
}
点击查看答案
#![allow(unused)]
fn main() {
struct Excerpt<'a> {
    part: &'a str,
}
}

解析: 结构体有引用字段,需要生命周期参数 'a

练习 3: 方法中的生命周期

为以下方法添加生命周期:

struct Excerpt<'a> {
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    // TODO: 添加生命周期标注
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}
点击查看答案
impl<'a> Excerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

解析: 根据省略规则 3,&self 的生命周期自动赋给返回值,无需额外标注。

练习 4: 理解 'static

解释以下代码为什么可以工作:

fn get_greeting() -> &'static str {
    "Hello, world!"
}

fn main() {
    let greeting = get_greeting();
    println!("{}", greeting);
}
点击查看答案

答案: 字符串字面量 "Hello, world!" 的类型是 &'static str,它存储在程序的二进制文件中,整个程序运行期间都有效。

解析: 'static 表示引用在整个程序生命周期内有效,字符串字面量是典型例子。


故障排查

Q: 什么时候需要标注生命周期?

A: 当编译器无法自动推断时。常见场景:

  • 函数有多个引用参数且返回引用
  • 结构体包含引用字段
  • impl 块中的方法返回引用且依赖多个参数

Q: 生命周期参数名必须是 'a 吗?

A: 不必须。可以使用任何有效标识符:

fn longest<'lifetime>(x: &'lifetime str, y: &'lifetime str) -> &'lifetime str

但约定使用短名称:'a, 'b, 'r 等。

Q: 一个函数可以有多个生命周期参数吗?

A: 可以:

fn select_first<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x  // 返回值生命周期与 x 相同
}

Q: 生命周期和泛型有什么区别?

A:

  • 泛型:参数的类型
  • 生命周期:引用的作用域
fn example<T, 'a>(value: T, reference: &'a T) {
    // T 是类型参数
    // 'a 是生命周期参数
}

知识扩展 (选学)

生命周期子类型

生命周期可以有约束关系:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
    'b: 'a,  // 'b 至少和 'a 一样长
{
    x
}

含义'b: 'a 表示 'b 的生命周期包含 'a

静态生命周期限制

'static 不意味着"永远",而是"程序运行期间":

// 可以存储在静态内存
const GREETING: &'static str = "Hello!";

// 但不能持有对动态数据的 'static 引用
fn create_string() -> &'static String {
    let s = String::from("hello");
    &s  // ❌ 错误:s 在函数结束时被丢弃
}

生命周期与闭包

闭包中的生命周期通常自动推断:

let x = 5;
let closure = |y| y + x;  // x 的生命周期被捕获

小结

核心要点

  1. 生命周期: 引用的有效作用域,防止悬垂引用
  2. 标注语法: &'a Type 表示引用活至少 'a 这么久
  3. 省略规则: 编译器自动推断常见模式
  4. 结构体: 有引用字段必须标注生命周期
  5. 'static: 整个程序运行期间有效

关键术语

  • Lifetime: 生命周期,引用的作用域
  • Dangling Reference: 悬垂引用,指向已释放数据的引用
  • Lifetime Annotation: 生命周期标注,'a 语法
  • Lifetime Elision: 生命周期省略,编译器自动推断
  • 'static: 特殊生命周期,程序运行期间有效

下一步


术语表

English中文
Lifetime生命周期
Reference引用
Borrow借用
Scope作用域
Dangling悬垂的
Static静态的
Elision省略

完整示例:src/basic/ownership_sample.rs - 生命周期和借用
相关示例:src/basic/generic_sample.rs - 泛型中的生命周期标注


知识检查

快速测验(答案在下方):

  1. 这段代码能编译通过吗?
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
  1. 'static 生命周期意味着什么?

  2. 什么时候需要标注生命周期?

点击查看答案与解析
  1. ❌ 不能 - 需要生命周期标注 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
  2. 数据在整个程序运行期间都有效(如字符串字面量)
  3. 当编译器无法自动推断时(多个引用参数且返回引用)

关键理解: 生命周期标注告诉编译器引用的有效范围。

继续学习

  • 下一步:闭包 - 捕获环境变量的匿名函数
  • 进阶:泛型 - 生命周期与泛型结合
  • 回顾:所有权 - 生命周期基础

💡 记住:生命周期是 Rust 的安全保障。标注生命周期不是负担,而是编译器帮助你避免错误的工具!

结构体

开篇故事

想象你正在设计一个游戏角色。每个角色有名字、生命值、等级、装备等属性。你不会为每个属性创建单独的变量,而是把它们组合在一起形成一个"角色"。Rust 的结构体就是这样的工具箱 - 它把相关的数据打包在一起,让它们作为一个整体被管理。


本章适合谁

如果你已经理解了变量和所有权,现在想学习如何组织复杂的数据,本章适合你。结构体是 Rust 中最常用的数据组织方式,所有 Rust 程序员每天都在使用。


你会学到什么

完成本章后,你可以:

  1. 定义结构体并创建实例
  2. 使用字段初始化简写语法
  3. 实现结构体方法(关联函数)
  4. 理解所有权在结构体中的工作方式
  5. 使用结构体更新语法和元组结构体

前置要求

学习本章前,你需要理解:


第一个例子

让我们定义一个简单的矩形结构体:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    
    println!("矩形面积是:{} 平方像素", rect1.width * rect1.height);
}

发生了什么?

  • struct Rectangle - 定义了一个名为 Rectangle 的结构体
  • width: 30 - 创建实例时给字段赋值
  • rect1.width - 访问字段,使用点号

原理解析

1. 结构体是什么?

结构体是自定义数据类型,允许你将多个值组合成一个有意义的整体。

类比

结构体就像数据库中的一行记录。比如"用户"表中的一行:用户名、邮箱、年龄、激活状态 - 这些信息共同描述一个用户。

2. 定义结构体

struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

结构体字段规则

  • 每个字段有名称类型
  • 字段之间用逗号分隔
  • 最后一个字段也可以有逗号(推荐,方便添加字段)
  • 字段可以是任何类型(包括其他结构体)

3. 创建实例

创建结构体使用字段初始化语法

let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("alice"),
    active: true,
    sign_in_count: 1,
};

字段顺序重要吗?

// 这些是等价的!
let user1 = User {
    email: String::from("test@example.com"),
    username: String::from("bob"),
    active: true,
    sign_in_count: 1,
};

let user2 = User {
    username: String::from("bob"),
    active: true,
    email: String::from("test@example.com"),
    sign_in_count: 1,
};

✅ 顺序不重要!Rust 通过字段名匹配,不是位置。

4. 访问字段

使用点号访问:

let user = User {
    email: String::from("test@example.com"),
    username: String::from("alice"),
    active: true,
    sign_in_count: 1,
};

println!("用户名:{}", user.username);  // alice
println!("邮箱:{}", user.email);       // test@example.com

user.active = false;  // ✅ 可以修改(如果变量是可变的)

所有权规则

let user1 = User {
    email: String::from("test@example.com"),
    username: String::from("alice"),
    active: true,
    sign_in_count: 1,
};

let email = user1.email;  // email 获得 String 的所有权
println!("{}", email);     // ✅ 可以
// println!("{}", user1.email); // ❌ 错误!email 已经移动了

5. 字段初始化简写

当变量名和字段名相同时,可以简写:

fn build_user(email: String, username: String) -> User {
    User {
        email: email,     // 重复
        username: username, // 重复
        active: true,
        sign_in_count: 1,
    }
}

// 简写版本
fn build_user(email: String, username: String) -> User {
    User {
        email,      // ✅ 简写!
        username,   // ✅ 简写!
        active: true,
        sign_in_count: 1,
    }
}

为什么这样设计?

  • 减少重复代码
  • 参数名和字段名通常相同
  • 代码更清晰

6. 结构体更新语法

使用已有实例创建新实例:

let user1 = User {
    email: String::from("test@example.com"),
    username: String::from("alice"),
    active: true,
    sign_in_count: 1,
};

let user2 = User {
    email: String::from("another@example.com"),
    ..user1  // 其他字段从 user1 复制
};

发生了什么?

  • email 使用了新值
  • username, active, sign_in_countuser1 移动user2

注意

// println!("{}", user1.username); // ❌ 错误!已经移动给 user2
println!("{}", user1.email); // ✅ 可以,email 是新创建的

7. 元组结构体

当结构体只有一个字段,或者你不想给字段命名时:

struct Color(i32, i32, i32);  // RGB
struct Point(i32, i32, i32);  // 3D 坐标

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

println!("黑色:({},{},{})", black.0, black.1, black.2);
println!("原点:({},{},{})", origin.0, origin.1, origin.2);

使用场景

  • 当结构体就是一个包装类型
  • 字段有明显顺序(如坐标 x, y, z)
  • 不需要字段名

8. 单元结构体

没有任何字段的结构体:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

有什么用?

  • 实现 trait 但不存储数据
  • 标记类型(marker type)
  • 泛型编程中的占位符

9. 组合 vs 继承

为什么 Rust 没有继承?

如果你来自 Java、C++ 或 Python,可能会疑惑:为什么 Rust 没有 classextends?答案是 Rust 选择了组合而非继承

继承的问题

想象一家餐厅。老板规定"所有员工都必须会做饭"。这听起来合理,但如果你雇佣了一个收银员呢?收银员继承"员工"的行为,但不需要做饭。这就是脆弱基类问题——父类的改变会破坏子类。

// Java 继承的困境
class Employee {
    void cook() { /* 做饭 */ }
}

class Cashier extends Employee {
    // 收银员被迫"会做饭"?但实际不需要!
    // 父类改变会影响所有子类
}

组合的解决方案

Rust 用 trait + 组合解决这个问题。每个员工有不同的能力组合:

// Rust 的组合模式
trait Cook {
    fn cook(&self);
}

trait HandleCash {
    fn handle_cash(&self);
}

struct Chef;
struct Cashier;

impl Cook for Chef {
    fn cook(&self) {
        println!("制作美食");
    }
}

impl HandleCash for Cashier {
    fn handle_cash(&self) {
        println!("处理收银");
    }
}

三个关键差异

维度继承 (Java/C++)组合 (Rust/Go)
耦合度紧耦合,父类改动影响子类松耦合,trait 独立变化
灵活性单继承限制,难以混合行为自由组合多个 trait
可测试性需要模拟整个父类只需模拟依赖的 trait
代码复用通过继承链通过 trait + 组合
运行时行为编译时固定动态分发可选

实战对比:游戏角色

// Java: 继承链越来越深
class Character {
    void move() {}
}
class FlyingCharacter extends Character {
    void fly() {}
}
class SwimmingFlyingCharacter extends FlyingCharacter {
    void swim() {}  // 继承链爆炸!
}
// Rust: 灵活组合
trait Move { fn move(&self); }
trait Fly { fn fly(&self); }
trait Swim { fn swim(&self); }

struct Dragon;

impl Move for Dragon { fn move(&self) {} }
impl Fly for Dragon { fn fly(&self) {} }
impl Swim for Dragon { fn swim(&self) {} }
// 自由组合,无需继承链

最佳实践

  • ✅ 用 trait 定义行为接口
  • ✅ 用组合组装复杂对象
  • ✅ 用 impl Trait for Type 实现多态
  • ❌ 避免深层继承链
  • ❌ 避免为复用代码而继承

常见错误

错误 1: 忘记字段类型

struct User {
    username,  // ❌ 编译错误!
    email,
}

编译器输出:

error: expected `:`, found `,`
 --> src/main.rs:2:14
  |
2 |     username,  // ❌ 编译错误!
  |              ^ expected `:`

修复方法

添加类型注解:

struct User {
    username: String,  // ✅
    email: String,
}

错误 2: 移动后使用字段

let user1 = User {
    email: String::from("test@example.com"),
    username: String::from("alice"),
    active: true,
    sign_in_count: 1,
};

let user2 = User {
    email: String::from("another@example.com"),
    ..user1  // user1 的字段移动给 user2
};

println!("{}", user1.username); // ❌ 编译错误!

编译器输出:

error[E0382]: borrow of partially moved value: `user1`
  --> src/main.rs:14:20
   |
9  |       email: String::from("another@example.com"),
   |                            --------------------- value partially moved here
...
14 |     println!("{}", user1.username);
   |                    ^^^^^^^^^^^^^^ value borrowed here after partial move

修复方法

  1. 使用引用而不是移动

    let user2 = &user1; // 借用,不移动
    println!("{}", user1.username); // ✅ 可以
  2. 不要使用更新语法,手动复制所有字段:

    let user2 = User {
        email: String::from("another@example.com"),
        username: user1.username.clone(), // 克隆
        active: user1.active,
        sign_in_count: user1.sign_in_count,
    };

错误 3: 试图修改不可变结构体

let user = User {
    email: String::from("test@example.com"),
    username: String::from("alice"),
    active: true,
    sign_in_count: 1,
};

user.active = false; // ❌ 编译错误!

编译器输出:

error[E0594]: cannot assign to `user.active`, as `user` is not declared as mutable
  --> src/main.rs:10:5
   |
2  |     let user = User {
   |         ---- help: consider changing this to be mutable: `mut user`
...
10 |     user.active = false;
   |     ^^^^^^^^^^^^^^^^^^^ cannot assign

修复方法

声明为可变:

let mut user = User {  // 添加 mut
    email: String::from("test@example.com"),
    username: String::from("alice"),
    active: true,
    sign_in_count: 1,
};

user.active = false; // ✅ 现在可以了

动手练习

练习 1: 预测所有权

预测下面代码哪些会编译通过:

struct Person {
    name: String,
    age: u32,
}

fn main() {
    let p1 = Person {
        name: String::from("Alice"),
        age: 30,
    };
    
    let p2 = p1;
    
    println!("{}", p1.name); // A: 编译通过还是失败?
    println!("{}", p2.name); // B: 编译通过还是失败?
    println!("{}", p2.age);  // C: 编译通过还是失败?
}
点击查看答案

答案:

  • A: ❌ 失败 - p1.name 已经移动给 p2
  • B: ✅ 通过 - p2 拥有 name
  • C: ✅ 通过 - ageu32,实现了 Copy trait

解析: String 会被移动,但 u32 会复制。所以p1.age仍然可用,但p1.name不可用。


练习 2: 使用更新语法

使用更新语法补全代码,使得 user2email 不同,其他字段和 user1 相同:

struct User {
    email: String,
    username: String,
    active: bool,
}

fn main() {
    let user1 = User {
        email: String::from("test@example.com"),
        username: String::from("alice"),
        active: true,
    };
    
    let user2 = User {
        // TODO: 使用更新语法
    };
}
点击查看答案

答案:

let user2 = User {
    email: String::from("another@example.com"),
    ..user1
};

注意: user1.usernameuser1.active 会移动到user2


练习 3: 字段初始化简写

使用字段初始化简写重写函数:

fn create_point(x: i32, y: i32, z: i32) -> Point {
    Point {
        x: x,
        y: y,
        z: z,
    }
}
点击查看答案

答案:

fn create_point(x: i32, y: i32, z: i32) -> Point {
    Point {
        x,  // 简写!
        y,  // 简写!
        z,  // 简写!
    }
}

规则: 当变量名和字段名相同时,可以省略冒号和值。


故障排查 (FAQ)

Q: 什么时候应该用结构体,什么时候用元组?

A: 遵循这个原则:

使用结构体

  • 字段有明确含义(如 nameage
  • 需要字段名提高可读性
  • 字段可能变化或扩展

使用元组结构体

  • 字段是同类数据(如坐标 x, y, z)
  • 只需要一个简单的包装
  • 字段有固定顺序且很明显

示例:

// ✅ 结构体 - 字段有不同含义
struct Person {
    name: String,
    age: u32,
    email: String,
}

// ✅ 元组结构体 - 都是坐标
struct Point(i32, i32, i32);

Q: 如何让结构体可以打印(Debug)?

A: 使用 #[derive(Debug)] 属性:

#[derive(Debug)]
struct User {
    username: String,
    email: String,
}

let user = User {
    username: String::from("alice"),
    email: String::from("test@example.com"),
};

println!("{:?}", user);  // ✅ 可以打印
// println!("{}", user); // ❌ 仍然不可以,需要实现 Display trait

输出:

User { username: "alice", email: "test@example.com" }

Q: 结构体字段可以是私有的吗?

A: ✅ 可以!使用访问控制:

mod user_module {
    pub struct User {
        pub username: String,  // 公开
        email: String,         // 私有!
    }
    
    impl User {
        pub fn get_email(&self) -> &str {
            &self.email  // 模块内可以访问
        }
    }
}

默认是私有的

  • pub struct - 结构体本身公开
  • pub field - 字段公开
  • 没有 pub - 私有

知识扩展 (选学)

方法(关联函数)

结构体可以有方法 - 这是下一章节的内容,先预览一下:

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}

let rect = Rectangle::new(30, 50);  // 关联函数
println!("面积:{}", rect.area());  // 方法

生命周期标注

当结构体包含引用时,需要生命周期标注:

struct RectangleRef<'a> {
    width: &'a u32,
    height: &'a u32,
}

生命周期 'a 告诉编译器:引用的有效期至少和结构体一样长。

这是高级主题,后续章节会详细讨论。


小结

核心要点

  1. 结构体组合相关数据 - 像数据库记录一样组织信息
  2. 字段初始化简写 - 当变量名和字段名相同时可以省略
  3. 更新语法 ..instance - 从已有实例创建新实例
  4. 所有权规则适用 - 字段可以移动、借用、复制
  5. 元组结构体用于简单包装 - 当只需要组合不需要字段名时

关键术语

  • Struct (结构体): 自定义数据类型,包含命名字段
  • Field (字段): 结构体的数据成员
  • Instance (实例): 结构体的具体值
  • Tuple struct (元组结构体): 没有字段名的结构体
  • Field init shorthand (字段初始化简写): x 替代 x: x
  • Update syntax (更新语法): ..instance 复制其他字段

下一步

  • 继续:结构体方法 - 为结构体添加功能
  • 相关:枚举 - 另一种数据组合方式
  • 进阶:特征 - 定义行为接口

术语表

English中文
Struct结构体
Field字段
Instance实例
Tuple struct元组结构体
Field init shorthand字段初始化简写
Update syntax更新语法

完整示例:src/basic/rectangle.rs


延伸阅读

学习完结构体后,你可能还想了解:

选择建议:

继续学习

💡 提示:结构体是你每天都会在 Rust 中使用的工具。多练习创建、访问和组织数据,你会很快掌握它!


💡 小知识:结构体 vs 元组

元组的问题

// 元组表示用户
let user = ("Alice", 30, "alice@example.com");

// 访问字段 - 需要记住每个位置的含义
println!("姓名:{}", user.0);  // 0 是什么?
println!("年龄:{}", user.1);  // 1 是什么?
println!("邮箱:{}", user.2);  // 2 是什么?

结构体的优势

// 结构体表示用户
struct User {
    name: String,
    age: u32,
    email: String,
}

let user = User {
    name: String::from("Alice"),
    age: 30,
    email: String::from("alice@example.com"),
};

// 访问字段 - 名称自说明
println!("姓名:{}", user.name);    // ✅ 一目了然
println!("年龄:{}", user.age);     // ✅
println!("邮箱:{}", user.email);   // ✅

何时使用

  • 元组: 临时组合、返回值、模式匹配
  • 结构体: 有明确含义的数据、需要长期存储

元组结构体 (混合方案):

struct Color(i32, i32, i32);  // RGB
let black = Color(0, 0, 0);
println!("R: {}", black.0);  // 仍用数字索引

🌟 工业界应用:游戏角色系统

场景:RPG 游戏角色定义

struct Character {
    name: String,           // 角色名
    level: u32,             // 等级
    health: f32,            // 生命值 (0.0 - 100.0)
    experience: u64,        // 经验值
    inventory: Vec<Item>,   // 背包物品
    position: Position,     // 当前位置
}

struct Position {
    x: f32,
    y: f32,
    z: f32,  // 3D 坐标
}

struct Item {
    name: String,
    item_type: ItemType,
}

enum ItemType {
    Weapon,
    Armor,
    Potion,
}

// 使用示例
fn main() {
    let hero = Character {
        name: String::from("勇者"),
        level: 10,
        health: 85.5,
        experience: 1500,
        inventory: Vec::new(),
        position: Position { x: 0.0, y: 0.0, z: 0.0 },
    };
    
    println!("{} 等级 {}", hero.name, hero.level);
}

为什么用结构体

  1. 可读性 - 字段名称说明用途
  2. 类型安全 - 编译器检查字段类型
  3. 可维护性 - 添加新字段不影响现有代码
  4. 文档化 - 结构本身就是文档

🧪 动手试试:设计结构体

练习:为图书管理系统设计结构体

// TODO: 定义 Book 结构体
// 字段:title, author, year, isbn, available

// TODO: 定义 Library 结构体
// 字段:name, books (Vec<Book>)

// 提示:
// - ISBN 是字符串
// - year 是整数
// - available 是布尔值
// - books 是 vector
点击查看答案
struct Book {
    title: String,
    author: String,
    year: u32,
    isbn: String,
    available: bool,
}

struct Library {
    name: String,
    books: Vec<Book>,
}

// 使用示例
fn main() {
    let book = Book {
        title: String::from("Rust 编程"),
        author: String::from("张三"),
        year: 2024,
        isbn: String::from("978-7-121-12345-6"),
        available: true,
    };
    
    let library = Library {
        name: String::from("市图书馆"),
        books: vec![book],
    };
}

扩展思考

  • 如何表示借阅记录?
  • 如何处理多册同一本书?
  • 如何快速查找某本书?

内存布局可视化

1. 结构体内存布局

Rectangle struct (8 bytes):
+0x00        +0x04
+------------+------------+
| width(u32) | height(u32)|
|  4 bytes   |  4 bytes   |
+------------+------------+

说明:

  • u32 类型占用 4 字节
  • 无填充,紧密排列
  • 总计 8 字节

2. 字段访问模式

rect ──────────→ [Rectangle struct]
                   ├─ width: 10
                   └─ height: 20

rect.width  ───→ 直接字段访问 (10)
rect.height ───→ 直接字段访问 (20)

3. 方法调用流程

rect.area()
    │
    ↓
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
        // 10 * 20 = 200
    }
}

4. 结构体更新语法

let user1 = User { active: true, username: "alice" };

let user2 = User {
    username: "bob",  // 新值
    ..user1           // 其他字段从 user1 复制
};

内存布局:
user1 无效 (username 已转移)
user2 有效 (拥有新 username)

知识检查

问题 1 🟢 (字段访问)

如何修改结构体字段的值?

struct Point {
    x: i32,
    y: i32,
}

let mut p = Point { x: 5, y: 10 };
// 如何将 x 改为 15?
答案与解析

答案: p.x = 15;

解析: 需要 mut 标记变量可变,然后通过点号访问修改字段。

问题 2 🟡 (方法语法)

以下哪种方法是正确的?

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

A) 只有 area 正确
B) 只有 square 正确
C) 都正确
D) 都不正确

答案与解析

答案: C) 都正确

解析: area 是实例方法(使用&self),square 是关联函数(构造器模式)。

问题 3 🔴 (结构体更新)

这段代码的输出是什么?

#[derive(Debug)]
struct User {
    active: bool,
    username: String,
}

let user1 = User {
    active: true,
    username: String::from("alice"),
};

let user2 = User {
    username: String::from("bob"),
    ..user1
};

println!("{}", user1.active);
答案与解析

答案: 编译错误!

解析: String 不是 Copy 类型,..user1 会转移 username 的所有权,导致 user1 无效。

修复: 使用 ..user1.clone() 或改用 &str

结构体字段

开篇故事

想象你正在填写一个表格。表格有"姓名"、"年龄"、"邮箱"等栏目。每个栏目只能填写特定类型的信息。Rust 结构体的字段就像这些表格栏目 - 它们定义了结构体可以存储什么类型的数据。


本章适合谁

如果你已经学完了结构体基础,现在想深入了解如何定义、访问和操作结构体的字段,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 定义结构体字段的类型
  2. 使用公有 (pub) 和私有字段
  3. 理解字段所有权的移动规则
  4. 使用字段初始化简写语法

前置要求


第一个例子

struct User {
    username: String,  // 用户名
    email: String,     // 邮箱
    active: bool,      // 是否激活
    sign_in_count: u64, // 登录次数
}

let user = User {
    username: String::from("alice"),
    email: String::from("alice@example.com"),
    active: true,
    sign_in_count: 1,
};

println!("用户名:{}", user.username);

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
字段定义class: self.xprivate int x;int x;x: i32Rust 必须标注类型
字段访问obj.xobj.getX() / obj.xobj.xobj.xpub 字段Rust 可控制字段可见性
字段可见性公开(无控制)private 默认公开私有默认,需 pubRust 默认私有
字段所有权引用引用可复制或引用移动或 CopyRust 有所有权语义
字段简写field: fieldfieldRust 有简写语法

核心差异: Python 类字段动态,Java 用 getter/setter,C++ 公开字段,Rust 默认私有且必须标注类型。


原理解析

1. 字段类型规则

字段可以是任何 Rust 类型:

struct Example {
    text: String,
    number: i32,
    flag: bool,
    point: (i32, i32),     // 元组
}

嵌套结构体

struct Point { x: f64, y: f64 }

struct Circle {
    center: Point,  // 字段是结构体
    radius: f64,
}

2. 字段所有权

String 字段会移动:

let user2 = User { username: user1.username };
// println!("{}", user1.username); // ❌ 已移动

Copy 类型的字段会复制:

struct Point { x: i32, y: i32 } // i32 实现了 Copy

let p1 = Point { x: 10, y: 20 };
let p2 = Point { x: p1.x, y: p1.y };
println!("p1.x = {}", p1.x); // ✅ 可以

3. 字段初始化简写

变量名和字段名相同时可省略:

fn build_user(username: String, email: String) -> User {
    User {
        username,  // ✅ 简写 (等同于 username: username)
        email,
        active: true,
    }
}

4. 公有和私有字段

mod my_module {
    pub struct User {
        pub username: String,  // 公有
        email: String,         // 私有!
    }
}

5. 字段必须标注类型

struct Valid {
    name: String,  // ✅
}

// struct Invalid { name, } // ❌ 缺少类型

常见错误

错误 1: 缺少类型注解

struct User {
    username,  // ❌ 编译错误!
}

修复:添加类型

struct User {
    username: String,  // ✅
}

错误 2: 移动私有字段

let user = my_module::User {
    // email: ... ❌ 私有字段不可访问
};

修复:使用 pub 或提供公共方法

错误 3: 忘记字段

let user = User {
    username: String::from("alice"),
    // ❌ 缺少 email 和 active
};

动手练习

练习 1: 预测结果

struct Point { x: i32, y: i32 }

let p1 = Point { x: 10, y: 20 };
let p2 = Point { x: p1.x, y: p1.y };

println!("p1: ({}, {})", p1.x, p1.y); // 通过吗?
点击查看答案

通过 - i32 实现 Copy,p1 仍可访问

练习 2: 使用简写

// 重写:
Person {
    name: name,
    age: age,
}
点击查看答案
Person {
    name,  // ✅ 简写
    age,
}

故障排查

Q: 字段顺序重要吗?

A: 不重要!按名称匹配,不是位置。

Q: 字段可以是函数吗?

A: ✅ 可以!字段可以是函数指针。

Q: 如何获取字段数量?

A: 无法运行时获取,字段在编译时确定。


小结

要点

  1. 语法: 字段名:类型
  2. Copy 类型复制: 非 Copy 类型移动
  3. 公有/私有: pub 控制访问
  4. 字段简写: 变量名=字段名可省略

术语

  • Field (字段): 结构体数据成员
  • Copy trait: 决定是否复制
  • Visibility (可见性): 访问控制

下一步


术语表

English中文
Field字段
Public field公有字段
Private field私有字段

完整示例:src/basic/rectangle.rs


💡 提示:字段是你与结构体交互的主要方式!


知识检查

快速测验(答案在下方):

  1. 如何初始化结构体时省略字段?

  2. 结构体更新语法是什么?

  3. 元组结构体和普通结构体有什么区别?

点击查看答案与解析
  1. 不能省略 - 所有字段必须初始化(除非有默认值)
  2. Struct { field1: value, ..existing_instance }
  3. 元组结构体有匿名命名字段,普通结构体有命名字段

关键理解: 结构体更新语法可以减少重复代码。

延伸阅读

学习完结构体字段后,你可能还想了解:

选择建议:

继续学习

前一章: 结构体
下一章: 结构体方法

相关章节:

返回: 基础入门

结构体方法

开篇故事

想象你有了一辆自行车。自行车不仅是一堆零件的组合(车架、轮子),它还能做动作:可以蹬踏板加速、可以刹车减速。Rust 的方法就像是结构体的"动作" - 它让结构体不仅能存储数据,还能对外界做出反应。


本章适合谁

如果你已经学完了结构体和字段,现在想让结构体"动起来"(不仅仅是数据容器),本章适合你。这是迈向面向对象编程的关键一步。


你会学到什么

完成本章后,你可以:

  1. 为结构体实现方法
  2. 理解 self&self&mut self 的区别
  3. 创建关联函数(类似静态方法)
  4. 使用方法修改结构体字段

前置要求

学习本章前,你需要理解:


第一个例子

定义一个矩形结构体并添加计算面积的方法:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("面积是:{} 平方像素", rect.area());
}

输出

面积是:1500 平方像素

关键概念

  • impl - 实现块
  • &self - 方法的第一个参数(调用者)
  • 点号调用:rect.area()

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
方法定义def method(self):void method()void method()fn method(&self)Rust 用 impl
self 参数self 显式this 隐式this 隐式&self 显式Rust 显式且可选引用
可变方法无需声明无需声明无需声明需要 &mut selfRust 需显式可变借用
关联函数@staticmethodstatic method()static method()fn new() (无 self)Rust 无 static 关键字
方法链return selfreturn thisreturn *this&mut SelfRust 返回引用

核心差异: Python/Java/C++ 的 this/self 隐式传递,Rust 的 self 显式且需选择借用方式。


原理解析

1. 实现块 (impl)

使用 impl 关键字为结构体添加方法:

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

2. 方法接收者

方法的第一参数决定调用方式:

&self - 借用(不修改)

impl Rectangle {
    fn area(&self) -> u32 {  // 只读,不修改
        self.width * self.height
    }
}

let rect = Rectangle { width: 30, height: 50 };
println!("{}", rect.area()); // rect 仍然可用

&mut self - 可变借用(修改)

impl Rectangle {
    fn double_width(&mut self) {
        self.width *= 2;  // 修改字段
    }
}

let mut rect = Rectangle { width: 30, height: 50 };
rect.double_width();  // rect 现在 width=60

self - 获取所有权(消耗)

impl Rectangle {
    fn into_components(self) -> (u32, u32) {
        (self.width, self.height)  // 消耗 rect
    }
}

let rect = Rectangle { width: 30, height: 50 };
let (w, h) = rect.into_components(); // rect 不能再使用

3. 关联函数

关联函数(类似其他语言的静态方法):

impl Point {
    // 关联函数 (没有 self 参数)
    fn new(x: f64, y: f64) -> Point {
        Point { x, y }
    }
    
    // 方法 (有 self 参数)
    fn distance(&self, other: &Point) -> f64 {
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }
}

fn main() {
    let p1 = Point::new(0.0, 0.0);  // 关联函数
    let p2 = Point::new(3.0, 4.0);
    println!("距离:{}", p1.distance(&p2));  // 方法
}

4. 多个实现块

可以使用多个 impl 块:

impl Point {
    fn new(x: f64, y: f64) -> Point {
        Point { x, y }
    }
}

impl Point {
    fn distance(&self, other: &Point) -> f64 {
        // 实现
    }
}

注意:通常合并到一个 impl 块,除非使用 trait。

5. 方法链

通过返回 &mut Self 实现方法链:

impl Rectangle {
    fn set_width(&mut self, width: u32) -> &mut Self {
        self.width = width;
        self  // 返回自身引用
    }
    
    fn set_height(&mut self, height: u32) -> &mut Self {
        self.height = height;
        self
    }
}

let mut rect = Rectangle { width: 30, height: 50 };
rect.set_width(100).set_height(200);  // 链式调用

常见错误

错误 1: 忘记 &

impl Rectangle {
    fn area(self) -> u32 {  // ❌ 会消耗 rect
        self.width * self.height
    }
}

let rect = Rectangle { width: 30, height: 50 };
println!("{}", rect.area());
// println!("{}", rect.width); // ❌ 错误!rect 被消耗了

修复:使用 &self

fn area(&self) -> u32 {  // ✅ 借用,不消耗

错误 2: 忘记 mut

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    rect.double_width();  // ❌ 错误!rect 不是可变的
}

修复:声明为 mut

let mut rect = Rectangle { width: 30, height: 50 };
rect.double_width();  // ✅

错误 3: 错误使用关联函数

let p1 = Point { x: 0.0, y: 0.0 };
p1.new(1.0, 1.0);  // ❌ 错误!new 是关联函数

修复:使用结构体名调用

let p1 = Point::new(0.0, 0.0);  // ✅

动手练习

练习 1: 实现方法

Person 结构体实现 greet 方法:

struct Person {
    name: String,
}

impl Person {
    // TODO: 实现 greet 方法,打印"Hello, my name is {name}"
}

fn main() {
    let p = Person { name: String::from("Alice") };
    p.greet();
}
点击查看实现
fn greet(&self) {
    println!("Hello, my name is {}", self.name);
}

练习 2: 使用 &mut self

Counter 结构体实现 increment 方法:

struct Counter {
    count: u32,
}

impl Counter {
    // TODO: 实现 increment,使 count 加 1
}
点击查看答案
fn increment(&mut self) {
    self.count += 1;
}

故障排查 (FAQ)

Q: 什么时候用 &self,什么时候用 self

A:

  • &self (99% 情况): 只读操作,不需要修改
  • &mut self: 需要修改结构体
  • self: 需要消耗结构体,返回内部数据

Q: impl 块可以嵌套吗?

A: ❌ 不可以。但可以写在其他 impl 块内。

Q: 方法可以是私有的吗?

A: ✅ 可以!默认私有:

impl Rectangle {
    fn internal_helper(&self) { /* 私有方法 */ }
    pub fn public_method(&self) { /* 公有方法 */ }
}

小结

核心要点

  1. impl: 为结构体添加方法
  2. &self: 借用,最常用
  3. &mut self: 可变借用,修改字段
  4. self: 获取所有权,消耗实例
  5. 关联函数: 类似静态方法,没有 self

术语

  • Method (方法): 与结构体关联的函数
  • Receiver (接收者): 方法的第一个参数(self
  • Associated function (关联函数): 没有 self 的方法

下一步


术语表

English中文
Method方法
impl实现
Receiver接收者
Associated function关联函数

完整示例:src/basic/rectangle.rs


💡 提示:方法让结构体"活"起来 - 它们不仅能存储数据,还能处理数据!


知识检查

快速测验(答案在下方):

  1. 方法和函数有什么区别?

  2. &self&mut selfself 的区别?

  3. 关联函数和实例方法的区别?

点击查看答案与解析
  1. 方法在 impl 块中定义,第一个参数是 self
  2. &self 借用,&mut self 可变借用,self 获取所有权
  3. 关联函数没有 self 参数(如 new()),实例方法有 self

关键理解: 方法是附加到结构体上的函数,self 决定访问模式。

延伸阅读

学习完结构体方法后,你可能还想了解:

选择建议:

  • 想学习枚举 → 继续学习 枚举
  • 想学习特征 → 跳到 特征

继续学习

前一章: 结构体字段
下一章: 枚举

相关章节:

返回: 基础入门

枚举

开篇故事

想象你在设计一个交通信号系统。信号灯只有三种状态:红、黄、绿。你不会希望有人随意设置成"紫色"或"蓝色"。Rust 的枚举就是一种确保值只能是预定义选项之一的机制 - 它让不可能的状态无法表示。


本章适合谁

如果你已经学完了结构体,想学习如何表示"有限选项"的数据,本章适合你。枚举是 Rust 类型系统的重要组成部分,与模式匹配配合使用非常强大。


你会学到什么

完成本章后,你可以:

  1. 定义枚举类型
  2. 使用 match 进行模式匹配
  3. 理解 Option 和 Result 枚举
  4. 为枚举实现方法
  5. 使用带数据的枚举变体

前置要求


第一个例子

定义一个交通灯枚举:

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn main() {
    let light = TrafficLight::Red;
    
    match light {
        TrafficLight::Red => println("停车!"),
        TrafficLight::Yellow => println("准备"),
        TrafficLight::Green => println("行驶!"),
    }
}

关键概念

  • enum - 定义枚举类型
  • TrafficLight::Red - 枚举变体
  • match - 模式匹配

原理解析

1. 枚举定义

enum Direction {
    North,
    South,
    East,
    West,
}

每个变体(variant)都是该类型的一个可能值。

2. 带数据的变体

enum Message {
    Quit,  // 无数据
    Move { x: i32, y: i32 },  // 命名字段
    Write(String),  // 单个值
    ChangeColor(i32, i32, i32),  // 多个值
}

3. 模式匹配

fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("退出"),
        Message::Move { x, y } => println!("移动到 ({}, {})", x, y),
        Message::Write(text) => println!("写入:{}", text),
        Message::ChangeColor(r, g, b) => println!("颜色:{}, {}, {}", r, g, b),
    }
}

4. Option 枚举

Rust 用枚举处理空值:

enum Option<T> {
    Some(T),
    None,
}

let some_number = Some(5);
let some_char = Some('a');
let absent: Option<i32> = None;

5. Result 枚举

错误处理:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("除数不能为 0"))
    } else {
        Ok(a / b)
    }
}

6. 代数数据类型 (ADT)

什么是 ADT?

代数数据类型(Algebraic Data Types)源自函数式编程,它用数学方式描述数据结构:

  • Product Types(积类型): 所有字段同时存在 → 就是 struct
  • Sum Types(和类型): 每次只有一个变体有效 → 就是 enum
// Product Type: 结构体包含所有字段
struct Point { x: i32, y: i32 }  // x 和 y 同时存在

// Sum Type: 枚举只能选一个
enum Option<T> { Some(T), None }  // 要么 Some,要么 None

多语言枚举对比

不同语言对枚举的支持差异很大:

C 语言(只有名称,无数据):

enum Color { RED, GREEN, BLUE };  // 只能是整数常量

Java 枚举(单例对象):

enum Color { RED, GREEN, BLUE }  // 本质是类的实例

Go 语言(常数 + iota):

type Color int
const (
    RED Color = iota  // 0
    GREEN              // 1
    BLUE               // 2
)

Swift 枚举(类似 Rust):

enum Result<T, E> {
    case success(T)
    case failure(E)
}

Rust 枚举(完整 ADT):

enum Message {
    Quit,                              // 无数据
    Move { x: i32, y: i32 },          // 命名字段
    Write(String),                     // 携带值
    ChangeColor(i32, i32, i32),        // 多个值
}

为什么 ADT 强大?

  1. 类型安全: 编译器保证你处理了所有可能的情况
  2. 数据携带: 变体可以携带任意类型的数据,不只是整数
  3. 模式匹配: match 表达式可以解构并提取数据

Rust 的枚举不是简单的整数常量,而是真正的代数数据类型。这让 OptionResult 这样的类型成为可能,从根本上避免了空指针错误。


常见错误

错误 1: match 不完整

enum Color {
    Red,
    Green,
    Blue,
}

fn describe(c: Color) {
    match c {
        Color::Red => println!("红色"),
        Color::Green => println("绿色"),
        // ❌ 缺少 Blue!
    }
}

编译器输出:

error[E0004]: non-exhaustive patterns: `Color::Blue` not covered

修复: 处理所有变体

match c {
    Color::Red => println!("红色"),
    Color::Green => println("绿色"),
    Color::Blue => println("蓝色"),
}

错误 2: 忘记解包数据

let msg = Message::Write(String::from("hello"));

match msg {
    Message::Write => println("有数据"),  // ❌ 没有解包
    _ => {}
}

修复:

match msg {
    Message::Write(text) => println("数据:{}", text),  // ✅
    _ => {}
}

错误 3: if let 类型错误

let some_value = Some(5);

if let Some(x) = some_value {
    println!("值是 {}", x);  // ✅ 正确
}

// ❌ 错误用法
if some_value == Some(5) {  // 需要 PartialEq trait

动手练习

练习 1: 定义枚举

定义一个表示一周七天的枚举:

// TODO: 定义 WeekDay 枚举,包含周一到周日
点击查看答案
enum WeekDay {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

练习 2: 实现方法

为 WeekDay 实现 is_weekend 方法:

impl WeekDay {
    fn is_weekend(&self) -> bool {
        // TODO: 周六或周日返回 true
    }
}
点击查看答案
fn is_weekend(&self) -> bool {
    matches!(self, WeekDay::Saturday | WeekDay::Sunday)
}

故障排查 (FAQ)

Q: 枚举和结构体有什么区别?

A:

  • 结构体: 所有字段同时存在
  • 枚举: 每次只有一个变体有效

Q: 什么时候用枚举,什么时候用布尔值?

A:

  • 2 个选项 → bool
  • 3+ 个明确选项 → enum
  • 可能需要更多选项 → enum

Q: Option 和 null 有什么区别?

A:

  • null: 任何类型都可以是空,容易导致空指针错误
  • Option: 显式标记可能为空,编译器强制检查

小结

核心要点

  1. 枚举定义有限选项: 值只能是预定义的变体之一
  2. match 强制穷尽: 必须处理所有情况
  3. Option 替代 null: 安全的空值处理
  4. Result 处理错误: 函数式错误处理
  5. 变体可以带数据: 灵活的类型表示

术语

  • Enum (枚举): 有限选项的类型
  • Variant (变体): 枚举的一个可能值
  • Pattern matching (模式匹配): 根据变体执行不同代码

下一步


术语表

English中文
Enum枚举
Variant变体
Pattern matching模式匹配

完整示例:src/basic/traits_sample.rs


💡 提示:枚举让不可能的状态无法表示,这是 Rust 类型系统的美妙之处!


项目实例

虽然项目中 enums.md 示例较少,但 trait 实现和错误处理中大量使用枚举:

Result 枚举(实际使用)

// 数据库操作中的 Result 使用
// src/advance/sqlx_sample.rs

use sqlx::{Result, Error};

async fn query_data() -> Result<Vec<User>> {
    let users = sqlx::query("SELECT * FROM users")
        .fetch_all(&pool)
        .await?;  // 自动转换错误
    
    Ok(users)
}

Option 枚举(实际使用)

// 查找用户,可能不存在
fn find_user(id: i64) -> Option<User> {
    // 可能返回 Some(user) 或 None
}

// 使用 match 处理
match find_user(1) {
    Some(user) => println!("找到用户:{}", user.name),
    None => println("用户不存在"),
}

延伸阅读

学习完枚举后,你可能还想了解:

选择建议:

  • 想学习多态 → 继续学习 特征
  • 想练习 → 跳到 泛型

知识检查

快速测验(答案在下方):

  1. 枚举和结构体有什么区别?

  2. Option<T> 的两个变体是什么?

  3. 如何使用 match 处理枚举?

点击查看答案与解析
  1. 结构体 = 所有字段同时存在,枚举 = 每次只有一个变体
  2. Some(T)None
  3. 使用 match 表达式匹配每个变体并处理

关键理解: 枚举是 Rust 的类型安全替代品,替代其他语言的 null。

继续学习

前一章: 结构体
下一章: 特征 (Traits)

相关章节:

返回: 基础入门

特征 (Traits)

开篇故事

想象你开了一家手机店。店里有 iPhone、Android 手机、老人机。虽然它们不一样,但都能"打电话"和"发短信"。Rust 的特征就像这个"手机"的概念 - 它定义了"能做什么",而不关心具体是什么手机。


本章适合谁

如果你已经学完了结构体和枚举,现在想学习如何定义通用行为,本章适合你。特征是 Rust 泛型和代码复用的核心。


你会学到什么

  1. 定义和实现特征
  2. 使用特征作为约束
  3. 理解特征对象
  4. 实现标准库特征
  5. 使用默认方法实现

前置要求


第一个例子

定义一个可以"叫"的特征:

trait Speak {
    fn speak(&self) -> &str;
}

struct Dog;
struct Cat;

impl Speak for Dog {
    fn speak(&self) -> &str {
        "汪汪!"
    }
}

impl Speak for Cat {
    fn speak(&self) -> &str {
        "喵喵!"
    }
}

fn make_sound(animal: &impl Speak) {
    println!("{}", animal.speak());
}

原理解析

1. 特征定义

trait Summary {
    fn summarize(&self) -> String;
}

2. 特征实现

struct NewsArticle {
    headline: String,
    location: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, {}", self.headline, self.location)
    }
}

3. 默认方法

trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")  // 默认实现
    }
}

4. 特征约束

fn notify(item: &impl Summary) {
    println!("{}", item.summarize());
}

// 或者
fn notify<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

5. 多个约束

fn notify<T: Summary + Clone>(item: &T) {
    // T 必须同时实现 Summary 和 Clone
}

常见错误

错误 1: 忘记实现必需方法

trait Summary {
    fn summarize(&self) -> String;
}

struct Article;

impl Summary for Article {
    // ❌ 缺少 summarize 方法
}

错误 2: 方法签名不匹配

impl Summary for Article {
    fn summarize(&self) -> &str {  // ❌ 返回类型应该是 String
        "summary"
    }
}

错误 3: 自相矛盾的特征约束

fn process<T: Summary + !Summary>(item: &T) {
    // ❌ 逻辑矛盾
}

动手练习

练习 1: 定义特征

定义一个可飞行的特征:

// TODO: 定义 Fly 特征,包含 fly 方法
点击查看答案
trait Fly {
    fn fly(&self) -> &str;
}

小结

要点

  1. 特征定义行为: 描述类型能做什么
  2. 实现提供具体方法: impl Trait for Type
  3. 默认方法减少重复: 可选覆盖
  4. 特征约束限制泛型: T: Trait
  5. 特征对象实现多态: dyn Trait

下一步


术语表

English中文
Trait特征
Implement实现
Default method默认方法

💡 提示:特征是 Rust 多态的核心 - 掌握它就掌握了泛型编程的钥匙!


项目实例

实际代码示例

// src/basic/traits_sample.rs

/// Printable 特性示例
trait Printable {
    fn print(&self);
}

struct Person {
    name: String,
    age: u32,
}

/// 实现 Printable 特性
impl Printable for Person {
    fn print(&self) {
        println!("Name: {}, Age: {}", self.name, self.age);
    }
}

fn main() {
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
    };
    
    person.print();  // 输出:Name: Alice, Age: 30
}

特性继承

// 定义 Trait A
trait A {
    fn method_a(&self);
}

// Trait B 继承 Trait A
trait B: A {
    fn method_b(&self);
}

struct MyStruct;

impl B for MyStruct {
    fn method_b(&self) {
        println!("Implemented method_b");
    }
}

// 必须也要实现 A
impl A for MyStruct {
    fn method_a(&self) {
        println!("Implemented method_a");
    }
}

对象安全警告

// ⚠️ 注意:这个方法不是对象安全的
trait UnObjectSafeTrait {
    fn create() -> Self;  // Error: 不能对象安全
}

// ❌ 以下代码会失败:
// fn create_trait_object() -> Box<dyn UnObjectSafeTrait> {
//     Box::new(UnObjectSafeTrait::create())
// }

延伸阅读

学习完特征后,你可能还想了解:

  • 孤儿规则 - 为什么不能为外部类型实现外部 trait
  • 默认实现 - trait 方法默认行为
  • 关联类型 vs 泛型 - 何时使用哪个

选择建议:

  • 想学习运行时多态 → 继续学习 特征对象
  • 想学习编译时多态 → 跳到 泛型

知识检查

快速测验(答案在下方):

  1. trait 和接口的区别是什么?

  2. 如何为外部类型实现 trait?

  3. 默认方法实现的作用?

点击查看答案与解析
  1. Rust trait 更灵活,支持默认实现和关联类型
  2. 孤儿规则:trait 或类型至少有一个在当前 crate 中
  3. 提供默认行为,实现者可以选择覆盖

关键理解: trait 是 Rust 多态的核心机制。

继续学习

前一章: 枚举
下一章: 泛型

相关章节:

返回: 基础入门

特征对象 (Trait Objects)

开篇故事

想象你在经营一家动物园。你需要一个函数来让所有动物发出声音。如果用泛型,你需要为每种动物(猫、狗、鸟)创建单独的函数版本。但如果有特征对象,你可以创建一个"动物"容器,放入任何实现了 Animal 特征的动物,然后统一调用 make_sound()。这就是特征对象的核心思想:在运行时处理不同类型的值,只要它们实现相同的特征

特征对象是 Rust 实现运行时多态的方式。它让你可以编写更灵活、可扩展的代码,特别是在需要存储不同类型的集合时。


本章适合谁

如果你已经理解了特征(trait)和泛型,现在想学习如何在运行时处理多种类型,或者需要将不同类型的值存储在同一个集合中,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 理解特征对象和动态分发的概念
  2. 使用 dyn Trait 语法创建特征对象
  3. 区分静态分发(泛型)和动态分发(特征对象)
  4. 理解特征对象的安全性要求
  5. 在集合中使用特征对象

前置要求

学习本章前,你需要理解:

  • 特征 - 理解 trait 定义和实现
  • 泛型 - 理解泛型语法
  • 枚举 - 理解枚举类型

第一个例子

让我们看一个最简单的特征对象示例:

trait Animal {
    fn make_sound(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn make_sound(&self) {
        println!("汪汪!");
    }
}

impl Animal for Cat {
    fn make_sound(&self) {
        println!("喵喵!");
    }
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
    ];
    
    for animal in animals {
        animal.make_sound();  // 动态调用
    }
}

发生了什么?

第 17 行 Box<dyn Animal> 是特征对象:

  • dyn Animal: 任何实现了 Animal trait 的类型
  • Box: 堆分配,因为特征对象大小在编译时未知
  • Vec<Box<dyn Animal>>: 可以存储不同类型的动物

第 22 行 animal.make_sound() 在运行时决定调用哪个实现(动态分发)。

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
运行时多态默认支持 (鸭子类型)接口 + 类继承虚函数 + 虚表特征对象 (dyn Trait)Rust 显式声明,零成本抽象
类型擦除动态类型泛型类型擦除特征对象类型擦除Rust 编译时检查安全性
性能开销高 (动态查找)中 (虚表查找)低 (虚表查找)低 (虚表查找,可内联优化)Rust 可预测性能
集合存储异类list = [Cat(), Dog()]List<Animal>std::vector<Animal*>Vec<Box<dyn Animal>>Rust 编译时保证类型安全
编译时多态不支持泛型模板泛型 (静态分发)Rust 两种都支持

原理解析

1. dyn Trait 语法

// 特征对象类型
let animal: Box<dyn Animal>;

// 引用特征对象
fn process(animal: &dyn Animal) {
    animal.make_sound();
}

// 可变引用
fn modify(animal: &mut dyn Animal) {
    // 可以修改
}

关键点

  • dyn Trait: 特征对象类型(Rust 1.27+ 语法)
  • 旧语法 Box<Trait> 已废弃,使用 Box<dyn Trait>
  • 特征对象总是通过指针使用(Box, &, Rc 等)

2. 动态分发 vs 静态分发

内存布局对比

静态分发 (泛型) - 编译时特化:
┌─────────────────────────────────┐
│ fn make_sound_static<Dog>(d: Dog)│  ← 编译时生成专用版本
│ fn make_sound_static<Cat>(c: Cat)│  ← 编译时生成专用版本
└─────────────────────────────────┘
运行时: 直接调用,无额外开销


动态分发 (特征对象) - 运行时查表:
┌─────────────────────────────────────────────────────┐
│ Box<dyn Animal> (24 bytes on 64-bit)                │
├──────────────────┬──────────────────────────────────┤
│ 数据指针 (8B)    │ 虚表指针 (8B)                    │
│ ────────────────→│ ────────────────────────────────→│
│                  │                                  │
│  Dog {           │  VTable for Dog:                 │
│    name: "旺财"   │  ┌─────────────────────────────┐ │
│  }               │  │ make_sound: fn(Dog) @0x1234 │ │
│                  │  │ drop: fn(Dog) @0x5678       │ │
│                  │  │ size: 16                    │ │
│                  │  │ align: 8                    │ │
│                  │  └─────────────────────────────┘ │
└──────────────────┴──────────────────────────────────┘

运行时调用 animal.make_sound():
1. 读取虚表指针
2. 查找 make_sound 函数地址
3. 调用函数 (传入数据指针)

性能对比

特性静态分发(泛型)动态分发(特征对象)
调用开销0 (直接调用)~1-2 CPU 周期 (虚表查找)
内联优化✅ 可以❌ 通常不行
代码大小大 (每个类型一份)小 (共享虚表)
编译时间长 (单态化)
------------------------------------------------------
性能更快(内联优化)稍慢(虚表查找)
代码大小可能膨胀(单态化)更小(一份代码)
灵活性编译时确定类型运行时可切换类型
集合存储不能存不同类型可以存不同类型

3. 虚表 (vtable) 机制

特征对象在内存中包含:

  1. 指向实际数据的指针
  2. 指向虚表(vtable)的指针
// 概念示意图
Box<dyn Animal> {
    data: Dog,           // 实际数据
    vtable: &VTable {    // 虚表指针
        drop: fn(Dog),
        size: usize,
        align: usize,
        make_sound: fn(&Dog),
    }
}

vtable 内容

  • 方法指针
  • 类型的大小和对齐信息
  • 析构函数

4. 特征对象安全性

不是所有 trait 都能成为特征对象。必须满足对象安全规则:

// ✅ 对象安全的 trait
trait Animal {
    fn make_sound(&self);
    fn name(&self) -> &str;
}

// ❌ 不是对象安全的 trait
trait NotObjectSafe {
    // 规则 1: 不能有返回 Self 的方法
    fn clone(&self) -> Self;
    
    // 规则 2: 不能有泛型方法
    fn process<T>(&self, value: T);
    
    // 规则 3: 不能有 Self 在参数位置(除了 &self)
    fn compare(&self, other: &Self);
}

对象安全规则

  1. 方法不能返回 Self
  2. 方法不能有泛型参数
  3. Self 只能出现在 &self&mut self

5. 在集合中使用特征对象

trait Shape {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
}

struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }

impl Shape for Circle { /* ... */ }
impl Shape for Rectangle { /* ... */ }

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 5.0 }),
        Box::new(Rectangle { width: 4.0, height: 6.0 }),
    ];
    
    for shape in shapes {
        println!("面积:{}", shape.area());
        println!("周长:{}", shape.perimeter());
    }
}

关键点

  • 使用 Box<dyn Trait> 存储不同具体类型
  • 所有类型必须实现相同的 trait
  • 通过 trait 方法统一访问

初学者常见困惑

💡 这是很多学习者第一次遇到特征对象时的困惑——你并不孤单!

困惑 1: "泛型和特征对象都能实现多态,我该用哪个?"

解答: 选择取决于你的场景:

#![allow(unused)]
fn main() {
// 泛型(静态分发)- 编译时确定类型
fn make_sound<T: Animal>(animal: T) {
    animal.make_sound();  // 编译时为每个类型生成专用版本
}

// 特征对象(动态分发)- 运行时确定类型
fn make_sound(animal: &dyn Animal) {
    animal.make_sound();  // 运行时查表调用
}
}

选择指南:

场景推荐原因
类型在编译时已知泛型零运行时开销,可内联优化
需要存储不同类型的集合特征对象泛型无法做到
需要返回不同类型特征对象函数只能有一个返回类型
类型数量很多或不确定特征对象泛型会为每个类型生成代码
性能关键路径泛型避免虚表查找开销
需要跨 crate 边界的多态特征对象泛型需要知道具体类型

简单判断:

  • 如果类型固定且少 → 泛型
  • 如果类型动态变化 → 特征对象

困惑 2: "为什么 Box<dyn Trait> 需要 Box?能不能直接用 dyn Trait?"

解答: 特征对象的大小在编译时未知,所以不能直接存储在栈上:

内存布局:
Box<dyn Animal> (24 bytes on 64-bit)
├── 数据指针 (8 bytes)  → 指向堆上的具体类型 (Dog/Cat)
└── 虚表指针 (8 bytes)  → 指向该类型的 vtable

dyn Animal 本身大小未知:
- Dog 可能是 16 bytes
- Cat 可能是 24 bytes
- Bird 可能是 8 bytes
编译器不知道该分配多少空间!

为什么需要指针:

  • Box<dyn Trait> — 堆分配,24 字节(指针 + vtable 指针)
  • &dyn Trait — 借用,24 字节(同上)
  • dyn Trait — ❌ 编译错误,大小未知

困惑 3: "什么是虚表 (vtable)?为什么需要它?"

解答: 虚表是函数指针表,让运行时能找到正确的方法实现:

Box<dyn Animal> 指向:
┌─────────────────┐    ┌─────────────────────────────┐
│ 数据: Dog       │    │ VTable for Dog:             │
│   name: "旺财"   │    │  make_sound: fn(Dog) @0x1234│
└─────────────────┘    │  drop: fn(Dog) @0x5678       │
                       │  size: 16                    │
                       │  align: 8                    │
                       └─────────────────────────────┘

调用 animal.make_sound() 时:
1. 读取 vtable 指针
2. 查找 make_sound 函数地址 (0x1234)
3. 调用该函数,传入数据指针

为什么需要: 因为编译器不知道 animal 具体是 Dog 还是 Cat,所以通过 vtable 在运行时找到正确的方法。

困惑 4: "为什么有些 trait 不能作为特征对象?"

解答: 只有对象安全的 trait 才能作为特征对象:

#![allow(unused)]
fn main() {
// ✅ 对象安全:
trait Animal {
    fn make_sound(&self);  // 不依赖 Self
}

// ❌ 不是对象安全:
trait Clone {
    fn clone(&self) -> Self;  // 返回 Self — 编译器不知道返回多大!
}

// ❌ 不是对象安全:
trait Generic {
    fn process<T>(&self, value: T);  // 泛型 — 编译器不知道要生成多少种版本!
}
}

对象安全规则:

  1. 方法不能返回 Self(编译器不知道返回多大)
  2. 方法不能有泛型参数(编译器不知道要生成多少版本)
  3. Self 只能出现在 &self&mut self

困惑 5: "dyn 关键字是必须的吗?Box<Trait> 不行吗?"

解答: dyn 是 Rust 1.27+ 的语法,旧语法已废弃:

#![allow(unused)]
fn main() {
// 旧语法(已废弃,但还能用)
let obj: Box<dyn Animal>;

// 新语法(推荐)
let obj: Box<dyn Animal>;
}

为什么加 dyn: 明确告诉读者这是动态分发,避免和泛型混淆:

#![allow(unused)]
fn main() {
Box<dyn Animal>   // 动态分发 — 运行时查表
Box<AnimalImpl>   // 静态分发 — 编译时确定
}

常见错误

错误 1: 特征对象不是对象安全的

// ❌ 错误:trait 不是对象安全的
trait Cloneable {
    fn clone(&self) -> Self;  // 返回 Self
}

let obj: Box<dyn Cloneable>;  // 编译错误

// ✅ 正确:使用对象安全的 trait
trait Animal {
    fn make_sound(&self);
}

let obj: Box<dyn Animal>;  // ✅

编译器输出:

error[E0038]: the trait `Cloneable` cannot be made into an object
 --> src/main.rs:5:14
  |
5 | let obj: Box<dyn Cloneable>;
  |              ^^^^^^^^^^^^^
  |
  = note: the trait cannot require a method that returns `Self`

错误 2: 忘记使用 Box

// ❌ 错误:特征对象大小未知
let animal: dyn Animal = Dog;

// ✅ 正确:使用指针
let animal: Box<dyn Animal> = Box::new(Dog);
let animal_ref: &dyn Animal = &Dog;

错误 3: 混用泛型和特征对象

// ❌ 错误:语法混淆
fn process<T: Animal>(animal: T) {
    // 这是泛型,不是特征对象
}

fn process_obj(animal: &dyn Animal) {
    // 这是特征对象
}

// ✅ 根据需求选择

错误 4: 特征对象没有 dyn 关键字

// ❌ 旧语法(已废弃但仍可用)
let animal: Box<Animal> = Box::new(Dog);

// ✅ 新语法(推荐)
let animal: Box<dyn Animal> = Box::new(Dog);

动手练习

🟢 入门练习:创建特征对象

定义一个 Payment trait 并创建特征对象:

💡 编译器是你的老师:先尝试不使用 dyn,让编译器告诉你为什么需要它!

// TODO: 定义 Payment trait,包含 pay(&self) -> bool 方法

struct CreditCard;
struct PayPal;

// TODO: 为 CreditCard 和 PayPal 实现 Payment

fn main() {
    let payments: Vec<Box<dyn Payment>> = vec![
        Box::new(CreditCard),
        Box::new(PayPal),
    ];
    
    for payment in payments {
        if payment.pay() {
            println!("支付成功!");
        }
    }
}
点击查看答案
trait Payment {
    fn pay(&self) -> bool;
}

struct CreditCard;
struct PayPal;

impl Payment for CreditCard {
    fn pay(&self) -> bool {
        println!("使用信用卡支付");
        true
    }
}

impl Payment for PayPal {
    fn pay(&self) -> bool {
        println!("使用 PayPal 支付");
        true
    }
}

解析: Payment trait 是对象安全的(无 Self 返回,无泛型),可以作为特征对象。

🟡 中级练习:函数参数使用特征对象

编写一个接受特征对象的函数:

💡 提示:想想 &[T] 切片语法,特征对象的切片应该怎么写?

trait Drawable {
    fn draw(&self);
}

struct Circle;
struct Square;

impl Drawable for Circle {
    fn draw(&self) {
        println!("绘制圆形");
    }
}

impl Drawable for Square {
    fn draw(&self) {
        println!("绘制正方形");
    }
}

// TODO: 定义 draw_all 函数,接受 &dyn Drawable 切片

fn main() {
    let shapes: Vec<Box<dyn Drawable>> = vec![
        Box::new(Circle),
        Box::new(Square),
    ];
    
    draw_all(&shapes);
}
点击查看答案
fn draw_all(shapes: &[Box<dyn Drawable>]) {
    for shape in shapes {
        shape.draw();
    }
}

解析: 使用 &[Box<dyn Drawable>] 切片接受任意数量的可绘制对象。

练习 3: 理解对象安全性

判断以下 trait 是否可以作为特征对象:

// 1.
trait A {
    fn method(&self);
}

// 2.
trait B {
    fn method(&self) -> Self;
}

// 3.
trait C {
    fn method<T>(&self);
}

// 4.
trait D {
    fn method(&self, other: &Self);
}
点击查看答案

答案:

  1. ✅ 可以 - 对象安全
  2. ❌ 不可以 - 返回 Self
  3. ❌ 不可以 - 有泛型参数
  4. ❌ 不可以 - Self 在参数位置(不是 &self

解析: 只有 trait A 满足对象安全的所有规则。

练习 4: 泛型 vs 特征对象

重写以下代码,分别使用泛型和特征对象:

trait Printer {
    fn print(&self);
}

// TODO: 使用泛型实现 print_item
// TODO: 使用特征对象实现 print_item_dyn

fn main() {
    // 两种实现都应该能工作
}
点击查看答案
// 泛型版本(静态分发)
fn print_item<T: Printer>(item: &T) {
    item.print();
}

// 特征对象版本(动态分发)
fn print_item_dyn(item: &dyn Printer) {
    item.print();
}

解析:

  • 泛型版本:编译时生成具体代码,更快
  • 特征对象版本:运行时查表,更灵活

故障排查

Q: 什么时候使用特征对象而不是泛型?

A: 使用特征对象当:

  • 需要存储不同类型的集合
  • 需要在运行时决定类型
  • 需要 trait 对象的多态性

使用泛型当:

  • 类型在编译时已知
  • 性能关键路径
  • 不需要动态分发

Q: 特征对象的性能开销有多大?

A:

  • 每次方法调用有一次虚表查找(间接跳转)
  • 通常 1-2 个 CPU 周期,对大多数应用可忽略
  • 性能关键代码使用泛型

Q: 可以使用 &Trait 而不必 Box<dyn Trait> 吗?

A: 可以,但有区别:

  • &dyn Trait: 借用,不拥有所有权
  • Box<dyn Trait>: 拥有所有权
  • 根据所有权需求选择

Q: 特征对象可以是 Send + Sync 吗?

A: 可以,需要标注:

fn process(obj: &dyn Trait + Send + Sync) {
    // obj 可以跨线程发送
}

知识扩展 (选学)

多个 trait 约束

特征对象可以实现多个 trait:

trait Foo {
    fn foo(&self);
}

trait Bar {
    fn bar(&self);
}

// 同时实现 Foo 和 Bar
fn process(obj: &dyn Foo + Bar) {
    obj.foo();
    obj.bar();
}

From 特征对象转换

使用 AsRefInto 进行转换:

trait Animal {
    fn name(&self) -> &str;
}

struct Dog;

impl Animal for Dog {
    fn name(&self) -> &str {
        "Dog"
    }
}

let dog = Dog;
let animal: &dyn Animal = &dog;  // 自动转换

枚举 vs 特征对象

对于已知类型集合,枚举可能更好:

// 特征对象
let animals: Vec<Box<dyn Animal>> = vec![...];

// 枚举(更类型安全)
enum AnimalEnum {
    Dog(Dog),
    Cat(Cat),
}

小结

核心要点

  1. 特征对象: dyn Trait 实现运行时多态
  2. 动态分发: 通过虚表查表调用方法
  3. 对象安全: trait 必须满足规则才能成为特征对象
  4. Box 需求: 特征对象大小未知,需要指针
  5. vs 泛型: 灵活性 vs 性能的权衡

关键术语

  • Trait Object: 特征对象,运行时多态
  • Dynamic Dispatch: 动态分发,运行时查表
  • Vtable: 虚表,方法指针表
  • Object Safety: 对象安全,特征对象的约束
  • Type Erasure: 类型擦除,隐藏具体类型

下一步

  • 学习 高级特征
  • 理解 智能指针
  • 探索 枚举高级用法

术语表

English中文
Trait Object特征对象
Dynamic Dispatch动态分发
Static Dispatch静态分发
Vtable虚表
Object Safety对象安全
Type Erasure类型擦除
Monomorphization单态化

完整示例:src/basic/traits_sample.rs - 特征定义和实现
相关示例:src/basic/generic_sample.rs - 静态分发 vs 动态分发


知识检查

快速测验(答案在下方):

  1. 这段代码能编译通过吗?
trait Foo { fn bar(&self) -> Self; }
let obj: Box<dyn Foo>;
  1. dyn Trait 和泛型有什么区别?

  2. 什么是对象安全?

点击查看答案与解析
  1. ❌ 不能 - 返回 Self 的 trait 不是对象安全的
  2. dyn Trait 是运行时动态分发,泛型是编译时单态化
  3. 对象安全 = trait 可以作为特征对象使用(无 Self 返回、无泛型方法)

关键理解: 特征对象牺牲性能换取灵活性。

继续学习

  • 下一步:特征 - 回顾特征基础
  • 进阶:模块系统 - 组织复杂代码结构
  • 回顾:泛型 - 静态分发 vs 动态分发

💡 记住:特征对象是 Rust 实现运行时多态的工具。优先使用泛型(静态分发),在需要灵活性时使用特征对象(动态分发)!

泛型 (Generics)

开篇故事

想象你有一个模具,可以用它制作不同材质的杯子——玻璃杯、陶瓷杯、塑料杯。模具本身不关心材质,它定义了杯子的形状,材质由使用者决定。Rust 的泛型就是这样——它让你编写与具体类型无关的代码,让编译器在实际使用时生成特定类型的版本。


本章适合谁

如果你已经理解了结构体和特征,现在想编写可复用的通用代码,本章适合你。泛型是高级 Rust 程序员的必备技能。


你会学到什么

完成本章后,你可以:

  1. 定义泛型函数和结构体
  2. 理解单态化 (monomorphization) 过程
  3. 使用特征约束限制泛型类型
  4. 使用 where 子句简化复杂约束
  5. 区分泛型与特征对象的使用场景

前置要求

学习本章前,你需要理解:


第一个例子

// 泛型函数 - 可以处理任何类型
fn identity<T>(x: T) -> T {
    x  // 返回输入的值
}

fn main() {
    let num = identity(5);           // T 推断为 i32
    let text = identity(String::from("hello")); // T 推断为 String
    let flag = identity(true);       // T 推断为 bool
    
    println!("{} {} {}", num, text, flag);
}

关键点

  • <T>类型参数 - 调用时确定实际类型
  • 一个函数,多种用法 - 代码复用
  • 编译器生成 specialized 版本 - 零成本抽象

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
泛型定义无需声明Listtemplatefn foo(x: T)Rust 用 声明
类型检查运行时检查编译时检查编译时检查编译时检查Rust 和 Java/C++ 类似
泛型实现无泛型类型擦除编译时展开单态化Rust 和 C++ 都是零成本
约束机制T extends Base无强制约束T: TraitRust 用 trait 约束
多类型参数不需要Map<K, V>template<T, U>struct Pair<T, U>语法相似

核心差异: Python 无泛型概念(动态类型),Java 用类型擦除(运行时有开销),Rust 和 C++ 用单态化(编译时零成本)。


原理解析

数据支撑:为什么泛型很重要?

工业界数据:

  • 代码复用率: 使用泛型可减少 60-80% 的重复代码(对比为每种类型写单独函数)
  • 编译时间影响: 100 个泛型函数单态化后约增加 0.3-0.5 秒 编译时间(可接受)
  • 二进制大小: 单态化会使二进制增加约 5-15%(取决于泛型使用量)
  • 运行时性能: 泛型 vs 手写专用函数 — 零差异(编译时完全展开)

对比其他语言:

语言泛型实现方式运行时开销编译时开销类型安全
Rust单态化0%中等✅ 完全
C++模板展开0%高(错误信息复杂)✅ 完全
Java类型擦除5-10%(装箱/拆箱)⚠️ 部分(运行时类型检查)
C#JIT 特化0-2%中等✅ 完全
Python无(动态类型)高(动态查找)❌ 无

真实案例:

  • Tokio 异步运行时: 大量使用泛型(Future<T>, Stream<T>),单态化后性能与手写专用代码相当
  • Serde 序列化框架: 泛型 + 派生宏,JSON 解析速度比 Python 的 json 库快 5-20 倍(典型场景)

初学者常见困惑

💡 这是很多学习者第一次遇到泛型时的困惑——你并不孤单!

困惑 1: "单态化听起来很厉害,但到底是什么意思?"

解答: 想象你有一个"万能模具"(泛型函数)。当你第一次使用它制作 i32 类型的杯子时,编译器会为你创建一个专门的 i32 模具。当你再制作 String 类型的杯子时,又创建一个专门的 String 模具。每个模具都是专用的,所以做出来的杯子质量最好(运行时无开销)。

困惑 2: "Java 也有泛型,Rust 的泛型和 Java 有什么不同?"

解答: 关键差异在于实现方式:

  • Java: 类型擦除 — 编译后泛型信息被擦除,运行时用 Object 代替,需要类型转换(有开销)
  • Rust: 单态化 — 编译时生成专用版本,运行时无开销
Java 泛型: List<String> → 编译后 → List<Object> → 运行时转换
Rust 泛型: fn foo<T>(x: T) → 编译后 → fn foo_i32(x: i32) + fn foo_string(x: String)

1. 泛型函数

泛型让你编写与类型无关的代码:

// 没有泛型:需要为每种类型写一个函数
fn max_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

fn max_string(a: String, b: String) -> String {
    // 重复代码...
}

// 使用泛型:一个函数处理所有类型
fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

2. 单态化 (Monomorphization)

重要概念

编译时:
泛型代码<T>  +  具体类型 (i32, String)
     │
     ├── 特化为 max_i32(a: i32, b: i32) → i32
     └── 特化为 max_String(a: String, b: String) → String
     ↓
运行时:
max_i32(a, b)     ← 专用版本(无泛型开销)
max_String(a, b)  ← 专用版本(无泛型开销)

什么是单态化: Rust 在编译时将泛型代码特化为具体类型的版本。这意味着:

  • ✅ 运行时无性能损失
  • ✅ 类型安全在编译时检查
  • ❌ 代码量可能增加(每个类型一个版本)

3. 泛型结构体

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let p1 = Point { x: 5, y: 10 };           // Point<i32>
    let p2 = Point { x: 1.0, y: 2.0 };       // Point<f64>
    
    // 错误:x 和 y 必须是同一类型
    // let p3 = Point { x: 5, y: 1.0 };  // ❌
}

常见错误

错误 1:类型参数未使用

fn unused<T>(x: i32) -> i32 {
    x  // T 未使用
}

编译器输出

warning: type parameter `T` goes unused
 --> src/main.rs:1:12
  |
1 | fn unused<T>(x: i32) -> i32 {
  |            ^ help: remove the unused type parameter

修复方法

fn unused(x: i32) -> i32 { x }  // 移除未使用的 T
// 或
fn identity<T>(x: T) -> T { x } // 使用 T

错误 2:缺少特征约束

fn print_it<T>(x: T) {
    println!("{}", x);  // ❌ T 可能不能打印
}

编译器输出

error[E0277]: `T` doesn't implement `std::fmt::Display`
 --> src/main.rs:3:20
  |
3 |     println!("{}", x);
  |                    ^ `T` cannot be formatted with the default formatter

修复方法

use std::fmt::Display;

fn print_it<T: Display>(x: T) {  // 添加约束
    println!("{}", x);
}

错误 3:生命周期缺失

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

编译器输出

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:30
  |
1 | fn longest(x: &str, y: &str) -> &str {
  |            ----     ----        ^ expected named lifetime parameter

修复方法

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

动手练习

练习 1:泛型结构体

实现一个可以存储任何类型的 Box 结构体:

struct Box<T> {
    // TODO: 定义字段
}

impl<T> Box<T> {
    // TODO: 实现 new 和 get 方法
}

fn main() {
    let num_box = Box::new(42);
    let text_box = Box::new(String::from("hello"));
}
点击查看答案
struct Box<T> {
    inner: T,
}

impl<T> Box<T> {
    fn new(value: T) -> Self {
        Box { inner: value }
    }
    
    fn get(&self) -> &T {
        &self.inner
    }
}

解析:泛型结构体允许存储任何类型,但一旦创建,类型就固定了。

练习 2:多类型参数

实现一个可以容纳两种类型的 Pair 结构体:

struct Pair< T, U> {
    // TODO: 两个字段,可以是不同类型
}
点击查看答案
struct Pair<T, U> {
    first: T,
    second: U,
}

解析:泛型可以有多个类型参数,适合元组、键值对等场景。


故障排查 (FAQ)

Q: 泛型和动态特征对象有什么区别?

A:

// 泛型:编译时确定类型,零开销
fn gen<T: Trait>(x: T) { }  // 单态化

// 特征对象:运行时确定类型,有开销
fn dyn_obj(x: &dyn Trait) { }  // 虚表查找

选择

  • 需要性能 → 用泛型
  • 需要在集合中存储不同类型 → 用特征对象

Q: 何时使用 where 子句?

A: 当约束复杂时:

// 约束太多时
fn complex<T: Clone + Display + Debug + Default>() { }

// 用 where 更清晰
fn complex<T>()
where
    T: Clone + Display + Debug + Default,
{ }

延伸阅读

学习完本章,你可能还想了解:

选择建议

  • 需要类型级别优化 → const generics
  • 设计复杂特征 → associated types
  • 高级技巧 → PhantomData

知识检查

问题 1 🟢 (基础概念)

以下哪个说法正确?

A) 泛型在运行时确定类型
B) 泛型会导致运行时性能损失
C) 泛型在编译时单态化
D) 泛型不能有多个类型参数

答案与解析

答案: C) 泛型在编译时单态化

解析: Rust 的泛型在编译时将类型参数替换为具体类型,生成专门的代码版本。

问题 2 🟡 (特征约束)

这段代码有什么问题?

fn duplicate<T>(x: T) -> (T, T) {
    (x.clone(), x.clone())
}
答案与解析

答案: 缺少 Clone 约束

修复:

fn duplicate<T: Clone>(x: T) -> (T, T) {
    (x.clone(), x.clone())
}

解析: 不是所有类型都实现 Clone,需要显式约束。

问题 3 🔴 (边界情况)

以下代码的输出是什么?

fn process<T: Default>() -> T {
    T::default()
}

fn main() {
    let x: i32 = process();
    println!("{}", x);
}
答案与解析

答案: 0

解析: i32::default() 返回 0。这是一个空值初始化模式。


小结

核心要点

  1. 泛型允许类型参数化 - 编写与具体类型无关的代码
  2. 单态化产生专用版本 - 编译时替换,运行时零开销
  3. 特征约束限制可用类型 - T: Trait 语法
  4. where 子句简化复杂约束 - 多行约束更清晰
  5. 生命周期与泛型配合 - 确保引用有效性

术语

  • Generic (泛型): 类型或函数的模板
  • Type Parameter (类型参数): 占位符类型 T
  • Monomorphization (单态化): 编译时特化过程
  • Trait Bound (特征约束): 限制泛型类型的约束

下一步

  • 继续:闭包 - 可执行的泛型代码
  • 相关:特征对象 - 动态类型的替代方案

术语表

English中文
Generic泛型
Type Parameter类型参数
Monomorphization单态化
Trait Bound特征约束
Where Clausewhere 子句

完整示例:src/basic/generic_sample.rs


💡 提示:泛型是 Rust 零成本抽象的核心 - 编译时的工作绝不留给运行时!


单态化可视化

1. 编译时单态化

编译前 (泛型代码):
fn max<T: Ord>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

编译时 (单态化):
         ↓ 为 i32 生成专用版本
         ↓ 为 String 生成专用版本
         ↓

编译后 (专用代码):
fn max_i32(a: i32, b: i32) -> i32 { ... }    // 类型 T = i32
fn max_string(a: String, b: String) -> String { ... }  // 类型 T = String

关键点:

  • 编译时生成专用版本
  • 运行时无性能损失
  • 代码量可能增加

2. 特征约束图

T: Display + Clone

T 必须实现:
+-----------------+
|     Display     |
+-----------------+
|     Clone       |
+-----------------+
      ↑
    必须同时实现

3. where 子句可视化

fn process<T, U>()
where
    T: Display + Clone,
    U: Into<T>

约束关系:
U ───Into──→ T ───Display/Clone──→ 

4. 泛型实例化

泛型函数:
fn identity<T>(x: T) -> T { x }

调用时:
identity(5)      → T = i32
identity("hi")   → T = &str
identity(true)   → T = bool

编译器生成:
fn identity_i32(x: i32) -> i32 { x }
fn identity_str(x: &str) -> &str { x }
fn identity_bool(x: bool) -> bool { x }

5. 类型系统进阶

Newtype 模式(新类型模式)

使用元组结构体创建类型安全的包装器:

// ❌ 错误:容易混淆
fn process_user(id: u64, product_id: u64) {
    // id 和 product_id 都是 u64,容易传反
}

// ✅ 正确:使用 Newtype 模式
struct UserId(u64);
struct ProductId(u64);

fn process_user(user_id: UserId, product_id: ProductId) {
    // 类型安全,不会传反
}

let user = UserId(123);
let product = ProductId(456);
process_user(user, product);  // ✅ 类型检查

PhantomData(幽灵数据)

当你需要在泛型中使用类型参数,但实际不存储该类型的值时:

use std::marker::PhantomData;

// 标记结构体拥有的数据类型
struct Owned<T> {
    ptr: *mut u8,
    _marker: PhantomData<T>,  // 告诉编译器我们"拥有" T
}

impl<T> Drop for Owned<T> {
    fn drop(&mut self) {
        unsafe {
            // 释放内存
        }
    }
}

零大小类型 (ZST)

不占用任何内存空间的类型,常用于类型级编程:

// 零大小类型
struct Marker;  // sizeof(Marker) = 0

// 在泛型中使用
struct Container<T> {
    data: Vec<u8>,
    _marker: std::marker::PhantomData<T>,
}

// 不同标记不会增加内存开销
let a: Container<Marker> = Container { data: vec![1,2,3], _marker: PhantomData };
let b: Container<Marker> = Container { data: vec![4,5,6], _marker: PhantomData };
// a 和 b 的内存占用相同

类型状态模式 (Type-State Pattern)

使用类型系统在编译时防止无效状态:

// 类型状态模式:编译时防止无效操作
struct Draft;
struct Reviewed;
struct Published;

struct Article<State> {
    title: String,
    content: String,
    _state: std::marker::PhantomData<State>,
}

impl Article<Draft> {
    fn new(title: String, content: String) -> Self {
        Article { title, content, _state: PhantomData }
    }
    
    fn review(self) -> Article<Reviewed> {
        Article { title: self.title, content: self.content, _state: PhantomData }
    }
}

impl Article<Reviewed> {
    fn publish(self) -> Article<Published> {
        Article { title: self.title, content: self.content, _state: PhantomData }
    }
}

// 使用:编译时保证流程正确
let draft = Article::new("Title".into(), "Content".into());
let reviewed = draft.review();
let published = reviewed.publish();

// ❌ 编译错误:不能直接从 Draft 到 Published
// let published = draft.publish();

知识检查

问题 1 🟢 (基础概念)

以下哪个说法正确?

A) 泛型在运行时确定类型
B) 泛型会导致运行时性能损失
C) 泛型在编译时单态化
D) 泛型不能有多个类型参数

答案与解析

答案: C) 泛型在编译时单态化

解析: Rust 的泛型在编译时将类型参数替换为具体类型,生成 specialised 代码版本。

问题 2 🟡 (特征约束)

这段代码有什么问题?

fn duplicate<T>(x: T) -> (T, T) {
    (x.clone(), x.clone())
}
答案与解析

答案: 缺少 Clone 约束

修复:

fn duplicate<T: Clone>(x: T) -> (T, T) {
    (x.clone(), x.clone())
}

解析: 不是所有类型都实现 Clone,需要显式约束。

问题 3 🔴 (边界情况)

以下代码的输出是什么?

fn process<T: Default>() -> T {
    T::default()
}

fn main() {
    let x: i32 = process();
    println!("{}", x);
}
答案与解析

答案: 0

解析: i32::default() 返回 0。这是一个空值初始化模式。


继续学习

前一章: 特征
下一章: 闭包

相关章节:

返回: 基础入门

闭包 (Closures)

开篇故事

想象你有个魔法便签,可以记住周围的环境。比如你在厨房时,便签记住冰箱里有牛奶;你走到客厅,便签依然记得牛奶的事。Rust 的闭包就是这样——它能"捕获"周围环境的变量,带着这些记忆到任何地方执行。


本章适合谁

如果你已经学完函数基础,想了解更灵活的代码块传递方式,本章适合你。闭包是 Rust 函数式编程的核心。


你会学到什么

完成本章后,你可以:

  1. 定义和使用闭包
  2. 理解闭包捕获环境的方式(by ref, by mut ref, by value)
  3. 区分 Fn、FnMut、FnOnce trait
  4. 在迭代器中使用闭包
  5. 使用 move 关键字转移所有权

前置要求

学习本章前,你需要理解:


第一个例子

fn main() {
    // 最简单的闭包
    let add_one = |x| x + 1;
    
    let result = add_one(5);
    println!("6 = {}", result);  // 输出:6 = 6
    
    // 带类型注解的闭包
    let add_two = |x: i32| -> i32 { x + 2 };
    println!("7 = {}", add_two(5));  // 输出:7 = 7
}

关键点

  • |x| 是参数列表
  • 自动推断类型 - 更简洁
  • 可以存储在变量中 - 一等公民

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
闭包语法lambda x: x + 1x -> x + 1[](int x) { return x+1; }`x
捕获环境自动捕获(引用)需声明 final 变量显式 [&][=]自动推断借用/移动Rust 编译器自动选择
类型标注不需要需明确类型可选可选或推断Rust 第一次调用后固定
修改环境nonlocal x不支持[&] 可修改需要 mutFnMutRust 用 trait 区分
存储闭包直接赋值需要接口类型std::functionBox<dyn Fn> 或泛型Rust 类型匿名

核心差异: Python 最灵活,Java 最受限,C++ 需显式指定捕获方式,Rust 编译器自动推断但用 trait 严格区分。


原理解析

1. 闭包语法

// 各种闭包形式
let add = |x, y| x + y;
let print = || println!("hello");
let square = |x: i32| -> i32 { x * x };

与函数的区别

// 函数:必须指定类型
fn add_fn(x: i32, y: i32) -> i32 {
    x + y
}

// 闭包:类型可推断
let add_closure = |x, y| x + y;

2. 捕获环境

三种捕获方式

fn main() {
    let x = 42;
    
    // 1. 不可变借用 (&x)
    let print_x = || println!("{}", x);
    
    // 2. 可变借用 (&mut x)
    let mut counter = 0;
    let mut increment = || counter += 1;
    
    // 3. 转移所有权 (move)
    let data = String::from("hello");
    let use_data = move || println!("{}", data);
}

3. Fn Trait 层次

闭包自动实现三个 trait,取决于如何捕获环境:

// 示例来自 src/basic/closure_sample.rs

// Fn: 只读捕获,可多次调用
let captured_value = 10;
let add_captured = |x: i32| x + captured_value;  // 实现 Fn
let result1 = add_captured(5);   // 输出: 15
let result2 = add_captured(20);  // 输出: 30 (多次调用)

// FnMut: 可变捕获,需 mut 声明
let mut mutable_value = 0;
let mut increment = |x: i32| {
    mutable_value += x;
    mutable_value
};
let incr1 = increment(5);   // 输出: 5
let incr2 = increment(10);  // 输出: 15

// FnOnce: 消耗所有权,仅一次调用
let owned_value = String::from("Owned");
let consume_string = move || owned_value;  // 实现 FnOnce
let consumed = consume_string;  // 第一次调用
// consume_string;  // ❌ 错误: 已被消耗

Trait 定义:

// 标准库 trait 定义 (简化版)
trait Fn {
    fn call(&self, args: Args) -> Output;  // 不可变借用 self
}

trait FnMut {
    fn call_mut(&mut self, args: Args) -> Output;  // 可变借用 self
}

trait FnOnce {
    fn call_once(self, args: Args) -> Output;  // 消耗 self
}

关系图:

Fn: &self  ──可以──> FnMut: &mut self ──可以──> FnOnce: self
     ^                                              ^
     |                                              |
     +────────────只能降级调用────────────────────────+

解释:
- 实现 Fn 的闭包 → 自动实现 FnMut 和 FnOnce
- 实现 FnMut 的闭包 → 自动实现 FnOnce
- 实现 FnOnce 的闭包 → 不一定实现 Fn 或 FnMut

4. 闭包作为函数参数

闭包可以作为参数传递,让函数接受"行为"而非仅数据:

// 示例来自 src/basic/closure_sample.rs

// 泛型函数接受闭包
fn apply<F>(f: F, value: i32) -> i32
where
    F: Fn(i32) -> i32,  // Fn trait 约束
{
    f(value)
}

let double = |x| x * 2;
let result = apply(double, 10);
println!("Doubled Result: {}", result);  // 输出: 20

// 闭包返回不同类型
fn process_and_print<F>(f: F, value: i32)
where
    F: Fn(i32) -> String,
{
    let result_string = f(value);
    println!("{}", result_string);
}

let stringify = |num: i32| format!("Number: {}", num);
process_and_print(stringify, 42);  // 输出: Number: 42

关键点:

  • F: Fn(i32) -> i32 是 trait 约束
  • 闭包可以作为参数传递给泛型函数
  • 函数可以接受不同行为的闭包

常见错误

错误 1:类型推断不一致

let closure = |x| x + 1;
let result1 = closure(5);      // ✅ 推断为 i32
let result2 = closure(5.0);    // ❌ 错误: 期望 i32 但找到 f64

编译器输出:

error[E0308]: mismatched types
  --> src/main.rs:3:24
   |
3 |     let result2 = closure(5.0);
   |                        ^^^ expected i32, found floating-point number

修复方法:

// 显式标注类型
let closure = |x: i32| x + 1;

解析: 闭包在第一次调用时推断类型,之后类型固定。

错误 2:可变借用冲突

let mut counter = 0;
let mut increment = || counter += 1;

println!("{}", counter);  // ❌ 错误: 与闭包的可变借用冲突
increment();

编译器输出:

error[E0502]: cannot borrow `counter` as immutable because it is also borrowed as mutable
  --> src/main.rs:5:20
   |
3 |     let mut increment = || {
4 |         counter += 1;
   |         ------- mutable borrow occurs here
5 |     println!("{}", counter);
   |                    ^^^^^^^ immutable borrow occurs here

修复方法:

let mut counter = 0;
let mut increment = || counter += 1;

increment();  // 先调用闭包,释放借用
println!("{}", counter);  // ✅ 借用已释放

解析: 闭包持有可变借用期间,不能有其他借用。

错误 3:move 后使用

let x = String::from("hello");
let closure = move || println!("{}", x);  // move 转移所有权
println!("{}", x);  // ❌ 错误: x 已被移动

修复方法:

let x = String::from("hello");
let closure = || println!("{}", &x);  // 借用,不转移
println!("{}", x);  // ✅ x 仍可用

解析: move 关键字强制转移所有权,捕获变量不再可用。

错误 4:FnOnce 被重复调用

let text = String::from("Hello");
let consume = move || text;  // 实现 FnOnce

consume();  // ✅ 第一次调用
consume();  // ❌ 错误: 闭包已被消耗

编译器输出:

error[E0382]: use of moved value: `consume`
  --> src/main.rs:6:5
   |
5 |     consume();
   |     ------- value moved here
6 |     consume();
   |     ^^^^^^^ value used here after move

修复方法:

// 方法 1: 不使用 move
let text = String::from("Hello");
let print = || println!("{}", &text);
print();
print();  // ✅ 可多次调用

// 方法 2: 克隆返回值
let text = String::from("Hello");
let print_clone = || {
    println!("{}", text);
    text.clone()
};

解析: FnOnce 闭包消耗捕获变量,只能调用一次。


动手练习

练习 1:闭包捕获环境

实现一个计算器闭包,捕获基础值并累加:

fn main() {
    let base = 10;
    
    // TODO: 定义 add_to_base 闭包,捕获 base
    // let add_to_base = ???;
    
    println!("{}", add_to_base(5));   // 应输出: 15
    println!("{}", add_to_base(20));  // 应输出: 30
}
点击查看答案
let base = 10;
let add_to_base = |x: i32| x + base;

println!("{}", add_to_base(5));   // 输出: 15
println!("{}", add_to_base(20));  // 输出: 30

解析: 闭包捕获 base 为不可变引用,实现 Fn trait,可多次调用。

练习 2:可变捕获闭包

实现一个累加器闭包,每次调用递增:

fn main() {
    let mut total = 0;
    
    // TODO: 定义 accumulate 闭包
    // let accumulate = ???;
    
    println!("{}", accumulate(5));   // 应输出: 5
    println!("{}", accumulate(10));  // 应输出: 15
    println!("{}", accumulate(3));   // 应输出: 18
}
点击查看答案
let mut total = 0;
let mut accumulate = |x: i32| {
    total += x;
    total
};

println!("{}", accumulate(5));   // 输出: 5
println!("{}", accumulate(10));  // 输出: 15
println!("{}", accumulate(3));   // 输出: 18

解析: 闭包可变借用 total,实现 FnMut trait,需要 mut 声明。

练习 3:闭包作为函数参数

实现 apply_operation 函数,接受不同操作的闭包:

fn apply_operation<F>(f: F, value: i32) -> i32
// TODO: 添加 trait 约束
{
    // TODO: 调用闭包
}

fn main() {
    let double = |x| x * 2;
    let triple = |x| x * 3;
    
    println!("{}", apply_operation(double, 10));  // 应输出: 20
    println!("{}", apply_operation(triple, 10));  // 应输出: 30
}
点击查看答案
fn apply_operation<F>(f: F, value: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    f(value)
}

fn main() {
    let double = |x| x * 2;
    let triple = |x| x * 3;
    
    println!("{}", apply_operation(double, 10));  // 输出: 20
    println!("{}", apply_operation(triple, 10));  // 输出: 30
}

解析: 泛型参数 F 约束为 Fn(i32) -> i32,可接受任何匹配的闭包。

练习 4:迭代器过滤

使用闭包过滤数字集合:

let numbers = vec![1, 2, 3, 4, 5, 6];

// TODO: 过滤出偶数
let evens: Vec<i32> = numbers
    .iter()
    .filter(|n| /* TODO */)
    .cloned()
    .collect();

println!("{:?}", evens);  // 应输出: [2, 4, 6]
点击查看答案
let numbers = vec![1, 2, 3, 4, 5, 6];

let evens: Vec<i32> = numbers
    .iter()
    .filter(|n| n % 2 == 0)
    .cloned()
    .collect();

println!("{:?}", evens);  // 输出: [2, 4, 6]

解析: filter 接受返回 bool 的闭包,闭包判断每个元素是否保留。


故障排查 (FAQ)

Q: Fn, FnMut, FnOnce 如何选择?

A: 作为参数时根据需求选择:

// Fn: 只读,可多次调用 (最灵活)
fn process_fn<F>(f: F) where F: Fn() {
    f();
    f();  // ✅ 可多次调用
}

// FnMut: 需修改环境
fn process_mut<F>(mut f: F) where F: FnMut() {
    f();  // ✅ 可修改环境
}

// FnOnce: 消耗所有权 (最严格)
fn process_once<F>(f: F) where F: FnOnce() {
    f();  // ✅ 仅一次调用
}

选择原则:

  • 只读访问 → Fn (推荐,最灵活)
  • 需要修改环境 → FnMut
  • 需要消耗所有权 → FnOnce (最严格)

Q: 闭包和函数有什么区别?

A:

// 函数: 不能捕获环境
fn add_fn(x: i32, factor: i32) -> i32 {
    x + factor
}

// 闭包: 可以捕获环境
let factor = 2;
let add_closure = |x| x + factor;  // 捕获 factor

// 函数类型: fn (函数指针)
let fn_ptr: fn(i32) -> i32 = add_fn;

// 闭包类型: 匿名,实现 Fn trait
// let closure_ptr: ??? = add_closure;  // 类型匿名

区别总结:

  • 函数不能捕获环境变量
  • 闭包可以捕获 (灵活)
  • 函数有明确类型 fn(T) -> R
  • 闭包类型匿名,通过 trait 表示

Q: move 何时必须使用?

A: 当闭包需要离开定义作用域时:

// 1. 返回闭包: 必须 move
fn create_closure() -> impl Fn() -> String {
    let text = String::from("Hello");
    move || text.clone()  // move 捕获 text
}

// 2. 线程: 必须 move
use std::thread;
thread::spawn(move || {
    // 独立线程需要所有权
});

// 3. 长时间存储: 建议 move
let closure = move || {
    // 避免生命周期问题
};

Q: 如何调试闭包类型?

A:

// 闭包类型是匿名的
let closure = |x| x + 1;
// 类型: impl Fn(i32) -> i32

// 需要存储时用 Box<dyn Fn>
let boxed: Box<dyn Fn(i32) -> i32> = Box::new(|x| x + 1);

// 需要具体类型时用函数指针
let fn_ptr: fn(i32) -> i32 = |x| x + 1;  // 不捕获环境的闭包

延伸阅读

学习完本章,你可能还想了解:

选择建议

  • 学习标准 API → Fn trait
  • 需要动态类型 → Box
  • 并行处理 → rayon

知识检查

问题 1 🟢 (基础概念)

以下代码的输出是什么?

let add_one = |x: i32| x + 1;
let result = add_one(5);
println!("Result: {}", result);

A) 5
B) 6
C) 编译错误
D) 运行时错误

答案与解析

答案: B) 6

解析: 闭包 add_one 将输入加 1,调用 add_one(5) 返回 5 + 1 = 6

问题 2 🟡 (捕获环境)

这段代码会输出什么?

let captured_value = 10;
let add_captured = |x: i32| x + captured_value;
let result = add_captured(5);
println!("Captured Result: {}", result);

A) 5
B) 10
C) 15
D) 编译错误

答案与解析

答案: C) 15

解析: 闭包捕获 captured_value = 10,调用 add_captured(5) 返回 5 + 10 = 15

问题 3 🟡 (Fn trait)

以下哪个闭包实现 FnOnce?

// A
let x = 5;
let a = || println!("{}", x);

// B
let mut y = 0;
let mut b = || y += 1;

// C
let z = String::from("hi");
let c = move || z;  // 返回 z
答案与解析

答案: C

解析:

  • A: 只读捕获 → 实现 Fn
  • B: 可变捕获 → 实现 FnMut
  • C: move 并返回所有权 → 实现 FnOnce (消耗 z)

问题 4 🔴 (高级场景)

如何修复这段代码使其可编译?

let mut counter = 0;
let mut increment = |x: i32| {
    counter += x;
    counter
};

fn apply_mut<F>(mut f: F, value: i32) -> i32
where
    F: FnMut(i32) -> i32,
{
    f(value)
}

apply_mut(&mut increment, 10);
println!("After apply_mut: {}", counter);  // ❌ 错误
答案与解析

答案: 在调用 apply_mut 后,increment 的借用已释放

let mut counter = 0;
let mut increment = |x: i32| {
    counter += x;
    counter
};

apply_mut(&mut increment, 10);
// 借用已释放,可以访问 counter
println!("Counter: {}", counter);  // ✅ 输出: 10

解析: apply_mut 接受 &mut increment,调用后借用释放,counter 可访问。


小结

核心要点:

  1. 闭包是匿名函数 - 简洁语法,类型推断,可存储在变量中
  2. 捕获环境变量 - 自动选择借用或移动,实现"记忆"功能
  3. 三种 trait 层次 - Fn (只读) < FnMut (修改) < FnOnce (消耗)
  4. 作为参数传递 - 泛型 + trait 约束,传递"行为"
  5. move 关键字 - 强制所有权转移,用于线程、返回等场景
  6. 类型推断机制 - 第一次调用时固定类型参数

源码示例对照:

源码位置概念示例
closure_sample.rs:4基本闭包`let add_one =
closure_sample.rs:24捕获环境`let add_captured =
closure_sample.rs:58FnMut`let mut increment =
closure_sample.rs:80FnOnce`let consume_string = move
closure_sample.rs:11函数参数fn apply<F>(f: F, value: i32) where F: Fn(i32) -> i32

术语:

  • Closure (闭包): 可捕获环境的匿名函数
  • Capture (捕获): 闭包记住环境变量的方式 (by ref, by mut ref, by value)
  • Environment (环境): 闭包定义时的作用域
  • Trait Bound (特征约束): 限制闭包能力的 trait (Fn/FnMut/FnOnce)
  • Move Keyword (move 关键字): 强制所有权转移

下一步:

  • 继续: 迭代器 - 闭包的最佳应用场景
  • 相关: 线程 - move 闭包的典型用法
  • 相关: 所有权 - 理解捕获机制的基础

术语表

English中文说明
Closure闭包可捕获环境的匿名函数
Capture Environment捕获环境闭包访问外部变量的机制
Fn TraitFn 特征只读捕获,可多次调用
FnMut TraitFnMut 特征可变捕获,可修改环境
FnOnce TraitFnOnce 特征消耗所有权,仅一次调用
Move Keywordmove 关键字强制所有权转移
Anonymous Function匿名函数无名称的函数定义
Type Inference类型推断编译器自动确定类型
Environment环境闭包定义时的作用域

项目实例

完整示例位于: src/basic/closure_sample.rs

代码示例覆盖:

  1. 基本闭包定义 (第 4-7 行)
  2. 闭包作为函数参数 (第 11-20 行)
  3. 捕获环境变量 (第 23-37 行)
  4. 返回不同类型 (第 40-51 行)
  5. FnMut 使用 (第 54-76 行)
  6. FnOnce 和 move (第 79-90 行)

运行示例:

# 在项目根目录执行
cargo run

# 输出包含所有闭包示例结果

💡 提示: 闭包是 Rust 函数式编程的核心 - 把"行为"当作"数据"传递,让代码更具表达力!


继续学习

前一章: 泛型
下一章: 线程与并发

相关章节:

返回: 基础入门

模块系统

开篇故事

想象你在经营一家大型超市。如果所有商品(食品、日用品、电器)都堆在一个大房间里,顾客会疯掉,员工也找不到东西。你需要把商品分类放在不同的区域:食品区、日用品区、电器区。每个区域有自己的入口,有些区域对所有顾客开放,有些区域(如仓库)只允许员工进入。

Rust 的模块系统就是超市的分区系统——它帮你组织代码,控制访问权限,让大型项目保持清晰和可维护。


本章适合谁

如果你正在编写超过 500 行的 Rust 项目,或者想学习如何组织多文件项目,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 使用 mod 创建模块层次结构
  2. 使用 pub 控制可见性
  3. 使用 use 简化路径
  4. 组织多文件项目结构
  5. 理解 pub(crate)pub(super)
  6. 避免循环依赖

前置要求


第一个例子

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
        fn seat_at_table() {}
    }
    
    mod serving {
        fn take_order() {}
    }
}

fn main() {
    // 调用模块内函数
    front_of_house::hosting::add_to_waitlist();
}

发生了什么?

  • mod - 定义模块
  • front_of_house::hosting - 模块路径
  • 默认情况下,模块是私有

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
模块定义文件即模块package + 文件夹namespacemod 关键字Rust 需显式声明 mod
可见性无强制控制public/private无强制控制pub + 默认私有Rust 默认私有
导入语法import moduleimport pkg.Classusing namespaceuse crate::moduleRust 用 use + 路径
嵌套模块import pkg.subpackage pkg.subnamespace ns::submod outer { mod inner }Rust 用树形结构
文件映射自动文件夹映射包无映射mod.rs 或同名文件Rust 有两种风格

核心差异: Python 最简单(文件即模块),Java 用 package + class,Rust 用 mod + 文件映射且默认私有。


原理解析

1. 模块树形结构

crate (根)
├── front_of_house
│   ├── hosting
│   │   ├── add_to_waitlist()
│   │   └── seat_at_table()
│   └── serving
│       └── take_order()
└── back_of_house
    ├── Breakfast (pub struct)
    └── Appetizer (pub enum)

2. 可见性规则

mod restaurant {
    // 私有模块(默认)
    mod kitchen {
        pub fn cook() {}  // 即使函数是 pub,模块私有也无法从外部访问
    }
    
    // 公有模块
    pub mod dining_area {
        pub fn seat_customer() {}  // 可以从外部访问
    }
}

fn main() {
    // ❌ 错误:kitchen 是私有模块
    // restaurant::kitchen::cook();
    
    // ✅ 正确:dining_area 是公有模块
    restaurant::dining_area::seat_customers();
}

3. pub 修饰符详解

pub mod public_module {        // 公开模块
    pub fn public_fn() {}      // 公开函数
    fn private_fn() {}         // 私有函数
    
    pub struct PublicStruct {  // 公开结构体
        pub field1: i32,       // 公开字段
        field2: String,        // 私有字段
    }
    
    pub enum PublicEnum {      // 公开枚举
        Variant1,              // 枚举变体总是公开的
        Variant2,
    }
}

// 受限可见性
pub(crate) mod crate_public {  // 只在 crate 内可见
    pub fn internal_api() {}
}

pub(super) mod parent_public {  // 只在父模块可见
    pub fn helper() {}
}

pub(in crate::my_module) mod restricted {  // 在特定路径可见
    pub fn limited_api() {}
}

4. use 导入

// 绝对路径
use std::collections::HashMap;

// 相对路径
use crate::front_of_house::hosting;

// 使用 super 访问父模块
mod parent {
    mod child {
        use super::sibling_function;
    }
}

// 重命名避免冲突
use std::fmt::Result;
use std::io::Result as IoResult;

// 导入多个
use std::{
    collections::HashMap,
    io::{self, Read, Write},
};

// 重新导出(pub use)
pub use crate::internal_module::PublicApi;

5. 多文件项目组织

my_project/
├── Cargo.toml
├── src/
│   ├── main.rs           // 入口点
│   ├── lib.rs            // 库根(如果有)
│   ├── models/           // 数据模型模块
│   │   ├── mod.rs        // 模块声明
│   │   ├── user.rs       // 用户模型
│   │   └── post.rs       // 帖子模型
│   ├── services/         // 业务逻辑模块
│   │   ├── mod.rs
│   │   ├── user_service.rs
│   │   └── post_service.rs
│   └── utils/            // 工具函数模块
│       ├── mod.rs
│       └── helpers.rs

src/models/mod.rs:

pub mod user;
pub mod post;

src/models/user.rs:

pub struct User {
    pub id: i32,
    pub name: String,
}

6. 模块最佳实践

// ✅ 好的模块设计
pub mod api {
    pub mod v1 {
        pub mod users {
            pub fn list() -> Vec<User> { vec![] }
        }
    }
}

// ✅ 使用 pub use 简化 API
pub mod prelude {
    pub use super::api::v1::users::list;
    pub use super::models::User;
}

// 用户现在可以这样使用:
// use my_crate::prelude::*;

常见错误

错误 1: 访问私有模块

mod restaurant {
    mod kitchen {  // 私有
        pub fn cook() {}
    }
}

fn main() {
    // ❌ 无法访问私有模块
    // restaurant::kitchen::cook();
    
    // ✅ 正确:将模块设为 pub
    // pub mod kitchen { ... }
}

错误 2: 忘记导入

fn main() {
    // ❌ 错误:未导入
    // let map = HashMap::new();
    
    // ✅ 正确:先导入
    use std::collections::HashMap;
    let map = HashMap::new();
}

错误 3: 循环依赖

// ❌ 错误:模块 A 依赖 B,B 依赖 A
mod a {
    use crate::b::function_b;  // 循环!
}

mod b {
    use crate::a::function_a;  // 循环!
}

// ✅ 正确:重构为单向依赖
mod common {
    pub fn shared_logic() {}
}

mod a {
    use crate::common::shared_logic;
}

mod b {
    use crate::common::shared_logic;
}

动手练习

练习 1: 创建花园模块

// TODO: 创建 garden 模块
// - 包含 Tree 和 Vegetable 结构体
// - Tree 是公开的,Vegetable 是私有的
// - 提供 public_api() 函数返回 Tree
点击查看答案
pub mod garden {
    pub struct Tree {
        pub name: String,
        height: f32,
    }
    
    struct Vegetable {
        name: String,
    }
    
    pub fn public_api() -> Tree {
        Tree {
            name: String::from("Oak"),
            height: 10.0,
        }
    }
}

练习 2: 组织多文件项目

// TODO: 设计以下项目结构
// library/
// ├── src/
// │   ├── lib.rs
// │   ├── models/
// │   │   ├── mod.rs
// │   │   ├── book.rs
// │   │   └── member.rs
// │   └── services/
// │       ├── mod.rs
// │       └── catalog.rs

故障排查

Q: mod.rs 和同名文件有什么区别?

A:

  • mod.rs - 旧风格(Rust 2015),模块内容在 mod.rs
  • module_name.rs - 新风格(Rust 2018+),模块内容在 module_name.rs
  • 推荐使用新风格

Q: 如何在测试中访问私有模块?

A: 使用 #[cfg(test)] 模块:

mod private_module {
    fn secret_function() -> i32 { 42 }
}

#[cfg(test)]
mod tests {
    use super::private_module::secret_function;
    
    #[test]
    fn test_secret() {
        assert_eq!(secret_function(), 42);
    }
}

Q: pub(crate) 和 pub 有什么区别?

A:

  • pub - 对所有 crate 可见
  • pub(crate) - 只对当前 crate 可见(内部 API)

知识扩展(选学)

模块与文件系统映射

Rust 2018+ 的模块解析规则:

// src/lib.rs
mod models;  // 查找 src/models.rs 或 src/models/mod.rs

// src/models.rs
pub mod user;  // 查找 src/models/user.rs

小结

要点

  1. mod 定义模块: 组织代码结构
  2. pub 控制可见: 默认私有
  3. use 导入名称: 简化路径
  4. 路径分层: crate::module::item
  5. 文件分离: 大模块放独立文件

术语表

English中文
Module模块
Visibility可见性
Path路径
Crate
Re-export重新导出
Prelude预导入模块

完整示例:src/basic/module_sample.rs


延伸阅读

学习完模块系统后,你可能还想了解:

选择建议:

知识检查

快速测验(答案在下方):

  1. moduse 的区别是什么?

  2. 如何使模块公开?

  3. pub(crate) 的作用是什么?

点击查看答案与解析
  1. mod 定义模块,use 引入名称到当前作用域
  2. 在模块前加 pubpub mod my_module
  3. 只在当前 crate 内可见,对外部 crate 私有

关键理解: 模块系统帮助组织大型项目和控制可见性。

继续学习

💡 提示:好的模块结构让代码像好文章一样易读!

线程与并发

开篇故事

想象你在经营一家餐厅。如果只有一个厨师(单线程),他必须按顺序完成每道菜:切菜 → 炒菜 → 装盘 → 下一道。这很慢,但不会出错。

如果你雇了多个厨师(多线程),他们可以同时做菜——但问题来了:如果两个厨师都想用同一把刀怎么办?如果一个厨师还没切完菜,另一个就拿走了怎么办?这就是并发编程的核心挑战:协调共享资源的访问

Rust 的线程系统就像一位经验丰富的餐厅经理——它在编译时就确保不会出现"抢刀"的情况。


本章适合谁

如果你想编写多线程程序提高性能,或者理解 Rust 如何防止数据竞争,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 使用 thread::spawn 创建线程
  2. 使用 join() 等待线程完成
  3. 使用 move 闭包转移所有权到线程
  4. 使用通道(channel)在线程间传递消息
  5. 使用 Arc<Mutex<T>> 安全共享可变状态
  6. 理解 Rust 的线程安全保证

前置要求


第一个例子

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

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("子线程:数字 {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    
    for i in 1..5 {
        println!("主线程:数字 {}", i);
        thread::sleep(Duration::from_millis(1));
    }
    
    handle.join().unwrap();  // 等待子线程完成
}

发生了什么?

  • thread::spawn - 创建新线程
  • 闭包在新线程中执行
  • join() - 阻塞等待线程完成
  • 主线程和子线程并发执行

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
线程创建threading.Thread()new Thread()std::thread`thread::spawn(
数据共享随意共享(无检查)随意共享(需同步)随意共享(需同步)编译时检查所有权Rust 编译时防止数据竞争
锁机制threading.Lock()synchronizedstd::mutexMutex<T> + ArcRust 锁包裹数据
消息传递Queue需要库需要库mpsc::channelRust 原生支持通道
线程安全运行时错误运行时错误运行时错误编译时保证Rust 无数据竞争

核心差异: Python/Java/C++ 线程安全靠程序员自觉,Rust 编译器强制检查,无数据竞争保证。


原理解析

1. 线程生命周期

创建                执行              完成
  │                  │                │
  ├─→ spawn() ───→ 运行中 ───→ join() ──→ 已结束
  │                  │                │
  │                  ↓                │
  │              panic!() ────────────→ 异常结束
  │
主线程继续...

2. 线程生命周期详解

创建阶段:
  thread::spawn(closure)
      │
      ├── 分配线程栈 (默认 2MB)
      ├── 调度器注册新线程
      └── 返回 JoinHandle

执行阶段:
  运行中
      │
      ├── 正常执行 → 返回结果
      ├── panic!() → 线程终止
      └── 被 join() → 阻塞调用者

完成阶段:
  join() 返回
      │
      ├── Ok(value) → 线程正常完成
      └── Err(panic) → 线程 panic

3. Move 闭包

let data = vec![1, 2, 3];

// ❌ 错误:data 是引用,可能在线程结束前被释放
let handle = thread::spawn(|| {
    println!("{:?}", data);
});

// ✅ 正确:使用 move 转移所有权
let handle = thread::spawn(move || {
    println!("{:?}", data);  // data 现在属于这个线程
});

// data 不能再在主线程中使用了!
// println!("{:?}", data);  // ❌ 编译错误

4. 消息传递(通道)

use std::sync::mpsc;  // multiple producer, single consumer
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    
    let handle = thread::spawn(move || {
        let msg = String::from("来自子线程的消息");
        tx.send(msg).unwrap();  // 发送消息
        // msg 所有权已转移,不能再使用
    });
    
    let received = rx.recv().unwrap();  // 接收消息(阻塞)
    println!("收到:{}", received);
    
    handle.join().unwrap();
}

通道特点

  • tx.send() - 发送消息(非阻塞)
  • rx.recv() - 接收消息(阻塞等待)
  • rx.try_recv() - 尝试接收(非阻塞)
  • 消息所有权转移到接收方

5. 共享状态(Arc + Mutex)

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc: 原子引用计数(多线程安全的 Rc)
    // Mutex: 互斥锁(保证同一时间只有一个线程访问)
    let counter = Arc::new(Mutex::new(0));
    
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("结果:{}", *counter.lock().unwrap());
}

6. Send 和 Sync Trait

// Send: 类型可以安全转移所有权到其他线程
// Sync: 类型可以安全通过引用共享给其他线程

// 大多数类型自动实现 Send 和 Sync
// 以下类型 NOT Send:
// - Rc<T> (引用计数不是原子的)
// - 原始指针 (*const T, *mut T)
// - Cell<T>, RefCell<T> (不是线程安全的)

常见错误

错误 1: 忽略 join handle

let handle = thread::spawn(|| {
    // 一些工作
});

// ❌ 忘记 join,线程可能未完成程序就退出
// handle.join().unwrap();

// ✅ 正确:总是 join
handle.join().unwrap();

错误 2: 数据竞争

use std::sync::Arc;
use std::thread;

let data = Arc::new(vec![1, 2, 3]);

// ❌ 错误:多个线程同时修改
let data_clone = Arc::clone(&data);
thread::spawn(move || {
    data_clone.push(4);  // ❌ 需要 Mutex
});

错误 3: 死锁

use std::sync::Mutex;

let mutex1 = Mutex::new(1);
let mutex2 = Mutex::new(2);

// ❌ 错误:两个线程以不同顺序获取锁
// 线程 1: mutex1.lock() → mutex2.lock()
// 线程 2: mutex2.lock() → mutex1.lock()
// 结果:死锁!

// ✅ 正确:总是以相同顺序获取锁

动手练习

练习 1: 并行计算

使用多线程计算向量中所有数字的和:

// TODO: 实现 parallel_sum 函数
// 将数据分成 4 块,每块用一个线程计算
// 最后汇总结果
点击查看答案
use std::thread;

fn parallel_sum(data: Vec<i32>) -> i32 {
    let chunk_size = data.len() / 4;
    let mut handles = vec![];
    
    for i in 0..4 {
        let chunk = data[i * chunk_size..(i + 1) * chunk_size].to_vec();
        let handle = thread::spawn(move || {
            chunk.iter().sum::<i32>()
        });
        handles.push(handle);
    }
    
    handles.into_iter().map(|h| h.join().unwrap()).sum()
}

故障排查

Q: Rust 线程和 Go goroutine 有什么区别?

A:

  • Rust 线程 = OS 线程(重量级,但性能好)
  • Go goroutine = 绿色线程(轻量级,由运行时调度)
  • Rust 需要 async/await 实现类似 goroutine 的轻量并发

Q: 如何限制线程数量?

A: 使用线程池(如 rayon crate):

use rayon::prelude::*;

let sum: i32 = (1..1000).par_iter().sum();

Q: Mutex 和 RwLock 有什么区别?

A:

  • Mutex: 同一时间只允许一个线程访问
  • RwLock: 允许多个读线程或一个写线程

小结

要点

  1. thread::spawn: 创建新线程
  2. join(): 等待线程完成
  3. 消息传递: 线程间安全通信
  4. 避免共享可变状态: 使用 Mutex 或通道

安全规则

  • ❌ 不要用 static mut
  • ❌ 不要在线程间共享可变引用
  • ✅ 使用 Arc<Mutex<T>> 安全共享
  • ✅ 使用通道 (channel) 传递消息

术语表

English中文
Thread线程
Data race数据竞争
Mutex互斥锁
Channel通道
Deadlock死锁
Send/Sync线程安全 trait

完整示例:src/basic/threads_sample.rs


知识检查

快速测验(答案在下方):

  1. Rc<T> 可以在多线程中使用吗?

  2. 这段代码有什么问题?

let data = vec![1, 2, 3];
let handle = thread::spawn(|| {
    println!("{:?}", data);
});
  1. 通道 (channel) 和 Mutex 有什么区别?
点击查看答案与解析
  1. ❌ 不能 - Rc 不是线程安全的,应该使用 Arc
  2. data 的所有权没有转移到闭包,需要使用 move
  3. 通道 = 消息传递(所有权转移),Mutex = 共享状态(借用)

关键理解: Rust 在编译时防止数据竞争。

继续学习

🔴 警告:并发编程容易出错。始终使用高级抽象(Arc、Mutex、通道),避免原始线程操作!

条件编译

开篇故事

想象你在组装一台电脑。不同地区需要不同的电源插头:美国用 110V 两脚插头,欧洲用 220V 圆脚插头。你不会为每个地区生产不同型号的电脑,而是设计一个通用主板,根据目标地区安装不同的电源模块。条件编译就是 Rust 的"电源适配器"——同一份代码,根据不同平台编译出不同的程序


本章适合谁

如果你需要编写跨平台代码(Windows/macOS/Linux),或者想实现可选功能(如日志、调试模式),本章适合你。条件编译是系统级编程的必备技能。


你会学到什么

完成本章后,你可以:

  1. 使用 #[cfg] 属性控制编译
  2. 编写平台特定代码
  3. 使用 cfg_if 宏简化条件编译
  4. 定义和使用特性标志(feature flags)
  5. 理解编译时 vs 运行时的区别

前置要求


第一个例子

#[cfg(target_os = "linux")]
fn get_platform_name() -> &'static str {
    "Linux"
}

#[cfg(target_os = "macos")]
fn get_platform_name() -> &'static str {
    "macOS"
}

#[cfg(target_os = "windows")]
fn get_platform_name() -> &'static str {
    "Windows"
}

fn main() {
    println!("当前平台:{}", get_platform_name());
}

发生了什么?

  • #[cfg(...)] - 条件编译属性
  • 只有匹配的平台代码会被编译
  • 其他平台代码完全不存在(零开销)

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
条件编译无(动态类型)#ifdef / #if#[cfg(...)]Rust 用属性声明
平台判断运行时检查运行时检查#ifdef _WIN32#[cfg(target_os = "linux")]Rust 编译时决定
编译时判断不支持不支持预处理器编译器Rust 无预处理器
特性标志feature = "logging"Cargo.toml 定义
代码消除部分消除完全消除Rust 零运行时开销

核心差异: Python/Java 无条件编译,C++ 用预处理器,Rust 用编译器属性且完全消除不匹配代码。


原理解析

1. cfg 属性语法

// 单个条件
#[cfg(target_os = "linux")]
fn linux_only() {}

// 多个条件(AND)
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn linux_x86_64() {}

// 多个条件(OR)
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn unix_like() {}

// 取反
#[cfg(not(debug_assertions))]
fn release_mode() {}

2. 常用条件变量

变量说明示例值
target_os操作系统"linux", "macos", "windows", "android"
target_archCPU 架构"x86_64", "aarch64", "arm"
target_family平台家族"unix", "windows"
debug_assertions调试模式仅在 cargo build 时为 true
feature特性标志feature = "logging"

3. cfg_if 宏

// 使用 cfg_if crate
cfg_if::cfg_if! {
    if #[cfg(target_os = "linux")] {
        use std::os::linux::raw;
    } else if #[cfg(target_os = "macos")] {
        use std::os::macos::raw;
    } else if #[cfg(target_os = "windows")] {
        use windows_sys::Win32::Foundation;
    } else {
        compile_error!("不支持的平台");
    }
}

优势

  • 避免重复 #[cfg] 属性
  • 更清晰的分支结构
  • 支持 elsecompile_error!

4. 特性标志(Feature Flags)

Cargo.toml:

[features]
default = ["logging"]
logging = []
debug_mode = []

代码中使用:

#[cfg(feature = "logging")]
fn log(message: &str) {
    println!("[LOG] {}", message);
}

#[cfg(not(feature = "logging"))]
fn log(_message: &str) {
    // 空实现,零开销
}

编译时启用:

# 启用 logging 特性
cargo build --features logging

# 禁用默认特性
cargo build --no-default-features

# 组合使用
cargo build --no-default-features --features debug_mode

常见错误

错误 1: cfg 语法错误

// ❌ 错误:缺少引号
#[cfg(target_os = linux)]
fn foo() {}

// ✅ 正确:字符串值需要引号
#[cfg(target_os = "linux")]
fn foo() {}

错误 2: 函数签名不匹配

// ❌ 错误:不同平台函数签名不同
#[cfg(target_os = "linux")]
fn get_path() -> String { "/home".to_string() }

#[cfg(target_os = "windows")]
fn get_path() -> &str { "C:\\" }  // 返回类型不同!

// ✅ 正确:保持相同签名
#[cfg(target_os = "linux")]
fn get_path() -> &'static str { "/home" }

#[cfg(target_os = "windows")]
fn get_path() -> &'static str { "C:\\" }

错误 3: 忘记处理所有情况

// ❌ 错误:只处理了 Linux
#[cfg(target_os = "linux")]
fn init() { /* ... */ }

fn main() {
    init();  // 在非 Linux 平台编译失败!
}

// ✅ 正确:提供默认实现
#[cfg(target_os = "linux")]
fn init() { /* Linux 特定初始化 */ }

#[cfg(not(target_os = "linux"))]
fn init() { /* 通用初始化 */ }

动手练习

练习 1: 平台信息打印

编写程序打印当前平台信息:

// TODO: 实现 get_platform_info() 函数
// 返回格式:"OS: xxx, Arch: xxx"
点击查看答案
fn get_platform_info() -> &'static str {
    cfg_if::cfg_if! {
        if #[cfg(target_os = "linux")] {
            let os = "Linux";
        } else if #[cfg(target_os = "macos")] {
            let os = "macOS";
        } else if #[cfg(target_os = "windows")] {
            let os = "Windows";
        } else {
            let os = "Unknown";
        }
    }
    
    cfg_if::cfg_if! {
        if #[cfg(target_arch = "x86_64")] {
            let arch = "x86_64";
        } else if #[cfg(target_arch = "aarch64")] {
            let arch = "ARM64";
        } else {
            let arch = "Unknown";
        }
    }
    
    // 简化版本
    #[cfg(target_os = "linux")]
    return "OS: Linux, Arch: x86_64";
    
    #[cfg(target_os = "macos")]
    return "OS: macOS, Arch: x86_64";
    
    #[cfg(target_os = "windows")]
    return "OS: Windows, Arch: x86_64";
    
    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
    return "OS: Unknown, Arch: Unknown";
}

练习 2: 调试模式日志

实现只在调试模式下打印日志的功能:

// TODO: 实现 debug_log() 宏
// 在 debug_assertions 为 true 时打印日志,否则什么都不做
点击查看答案
macro_rules! debug_log {
    ($($arg:tt)*) => {
        #[cfg(debug_assertions)]
        println!("[DEBUG] {}", format!($($arg)*));
        
        #[cfg(not(debug_assertions))]
        let _ = format!($($arg)*);  // 计算但不输出
    };
}

fn main() {
    debug_log!("用户登录:{}", "alice");
    debug_log!("数据库连接成功");
}

故障排查

Q: cfg 和 if 有什么区别?

A:

  • cfg编译时决定:不匹配的代码根本不存在
  • if运行时决定:所有代码都编译,运行时选择分支
// 编译时:不匹配的代码不会被编译
#[cfg(target_os = "linux")]
fn linux_only() {}

// 运行时:所有代码都编译,运行时判断
fn runtime_check(is_linux: bool) {
    if is_linux {
        // ...
    }
}

Q: 如何查看 cfg 展开后的代码?

A: 使用 cargo rustc -- --emit=mircargo expand

Q: 特性标志和环境变量有什么区别?

A:

  • 特性标志: 编译时决定,影响编译结果
  • 环境变量: 运行时读取,不影响编译

知识扩展(选学)

条件编译的实际应用

Tokio 异步运行时

// 只在 Unix 系统上支持信号
#[cfg(unix)]
use tokio::signal::unix::{signal, SignalKind};

// 只在 Windows 上支持控制台事件
#[cfg(windows)]
use tokio::signal::windows;

跨平台路径处理

#[cfg(unix)]
const PATH_SEPARATOR: &str = "/";

#[cfg(windows)]
const PATH_SEPARATOR: &str = "\\";

小结

核心要点

  1. cfg 属性: #[cfg(...)] 控制编译
  2. 特性标志: 可选功能,零开销
  3. 平台代码: target_os, target_arch
  4. cfg_if 宏: 简化条件编译

关键术语

  • Conditional Compilation: 条件编译
  • Feature Flag: 特性标志
  • Compile-time Decision: 编译时决定
  • Zero-cost Abstraction: 零开销抽象

术语表

English中文
Conditional compilation条件编译
Feature flag特性标志
Platform-specific平台特定
Compile-time编译时
Runtime运行时
Attribute属性

完整示例:src/basic/cfg_if_sample.rs


知识检查

快速测验(答案在下方):

  1. #[cfg]#[cfg_attr] 有什么区别?

  2. 如何在编译时定义自定义特性标志?

  3. cfg_if! 宏相比多个 #[cfg] 有什么优势?

点击查看答案与解析
  1. #[cfg] 条件编译代码,#[cfg_attr] 条件添加属性
  2. cargo rustc --cfg my_feature 或在 Cargo.toml 的 [features] 中定义
  3. cfg_if! 更清晰,支持 else 分支,避免重复 #[cfg]

关键理解: 条件编译是编译时决定,零运行时开销。

继续学习

💡 记住:条件编译让你的代码跨平台运行,但要保持函数签名一致!

指针与不安全代码

🔴 高危警告

本章涉及 Rust 的 unsafe 特性。这些内容仅用于理解 Rust 的底层机制。除非绝对必要且有充分理由,否则不要在生产线代码中使用 unsafe。


开篇故事

想象你在驾驶一辆汽车。安全模式就像有安全气囊、ABS 刹车辅助、车道偏离警告——系统会保护你不犯错。不安全代码就像关闭所有安全系统,直接操控引擎——你能获得极致性能,但一次失误就可能车毁人亡。

Rust 的 unsafe 就是那个"关闭安全系统"的开关。它不是"邪恶"的,而是强大但危险的工具。本章教你理解它、尊重它、必要时安全地使用它。


本章适合谁

如果你想理解 Rust 内存安全的底层机制,或者需要与 C 代码交互、实现高性能数据结构,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 理解原始指针语法和创建方式
  2. 掌握 unsafe 块的 5 种操作
  3. 识别何时必须使用 unsafe
  4. 使用安全抽象封装 unsafe 代码
  5. 理解未定义行为(UB)的危害

前置要求


第一个例子

fn main() {
    let mut num = 5;
    
    // ✅ 安全引用
    let r1 = &num;
    let r2 = &num;
    
    // ⚠️ 原始指针(unsafe)
    let r3 = &num as *const i32;
    let r4 = &mut num as *mut i32;
    
    // ❌ 解引用原始指针需要 unsafe
    unsafe {
        println!("r3 是:{}", *r3);
        *r4 = 10;  // ⚠️ 危险!
    }
    
    println!("num 现在是:{}", num);
}

发生了什么?

  • *const T - 不可变原始指针
  • *mut T - 可变原始指针
  • 创建指针是安全的,但解引用需要 unsafe

原理解析

1. 原始指针 vs 引用

let x = 10;
let ref_x = &x;          // 引用:安全,遵循借用规则
let ptr_x = &x as *const i32;  // 原始指针:不安全,无借用检查

// 引用保证:
// ✅ 永远不为空
// ✅ 指向有效数据
// ✅ 遵循借用规则(可变/不可变互斥)

// 原始指针不保证:
// ❌ 可能为空
// ❌ 可能指向已释放内存
// ❌ 可以同时有多个可变指针

2. 内存布局可视化

引用 vs 原始指针内存布局

安全引用 (&T):
┌─────────────────────────────────────────────┐
│ 栈 (Stack)                                  │
│ +─────────────────────────────────────────+ │
│ | ref_x: 0x7fff50001000 (地址)             | │
│ +─────────────────────────────────────────+ │
│         │                                   │
│         ▼                                   │
│ 堆 (Heap)                                   │
│ +─────────────────────────────────────────+ │
│ | 数据: 10                                 | │
│ +─────────────────────────────────────────+ │
│                                             │
│ 编译器保证:                                  │
│ ✅ ref_x 永远不为空                         │
│ ✅ ref_x 指向有效数据                       │
│ ✅ 遵循借用规则                             │
└─────────────────────────────────────────────┘


原始指针 (*const T):
┌─────────────────────────────────────────────┐
│ 栈 (Stack)                                  │
│ +─────────────────────────────────────────+ │
│ | ptr_x: 0x7fff50001000 (地址)             | │
│ +─────────────────────────────────────────+ │
│         │                                   │
│         ▼                                   │
│ 堆 (Heap) 或 任意内存地址                     │
│ +─────────────────────────────────────────+ │
│ | 数据: 10 或 无效数据                      | │
│ +─────────────────────────────────────────+ │
│                                             │
│ 无编译器保证:                                │
│ ❌ ptr_x 可能为空 (null)                    │
│ ❌ ptr_x 可能指向已释放内存 (悬垂指针)        │
│ ❌ ptr_x 可能指向未初始化内存                │
└─────────────────────────────────────────────┘

多语言指针/引用对比

语言指针/引用类型空值检查悬垂指针检查性能开销
Rust &T安全引用✅ 编译时✅ 编译时0
*Rust const T原始指针❌ 运行时❌ 运行时0
C/C++ T*原始指针0
Java T引用✅ 运行时 (NPE)✅ GC 防止
Python T引用✅ 运行时✅ GC 防止
*Go T指针✅ 运行时❌ (GC 保护)

关键差异:

  • Rust 安全引用在编译时保证安全,零运行时开销
  • C/C++ 指针完全无检查,程序员负责
  • Java/Python 在运行时检查,有 GC 开销
  • Go 有 GC 保护,但无编译时检查

3. unsafe 的 5 种操作

只有以下 5 种操作需要 unsafe

unsafe {
    // 1. 解引用原始指针
    let x = *ptr;
    
    // 2. 调用 unsafe 函数
    unsafe_function();
    
    // 3. 访问或修改可变静态变量
    STATIC_VAR = 10;
    
    // 4. 实现 unsafe trait
    impl UnsafeTrait for MyType {}
    
    // 5. 访问 union 的字段
    let field = my_union.variant1;
}

4. 何时必须使用 unsafe

合法场景

  1. FFI(外部函数接口)
// 调用 C 库
extern "C" {
    fn printf(format: *const i8, ...) -> i32;
}

unsafe {
    printf(b"Hello from C!\0".as_ptr() as *const i8);
}
  1. 高性能数据结构
// 实现 Vec 的底层
pub struct MyVec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
}

impl<T> MyVec<T> {
    pub fn push(&mut self, value: T) {
        unsafe {
            // 直接写入内存,跳过边界检查
            self.ptr.add(self.len).write(value);
        }
        self.len += 1;
    }
}
  1. 硬件操作
// 内存映射 I/O
const GPIO_BASE: *mut u32 = 0x40020000 as *mut u32;

unsafe {
    GPIO_BASE.write(0x01);  // 直接写硬件寄存器
}

5. 安全抽象封装

关键原则:unsafe 代码应该被安全的公共接口封装。

pub struct SafeBuffer {
    ptr: *mut u8,
    len: usize,
}

impl SafeBuffer {
    pub fn new(size: usize) -> Self {
        let ptr = unsafe {
            // unsafe 内部:分配内存
            alloc::alloc::alloc(std::alloc::Layout::from_size_align(size, 1).unwrap())
        };
        SafeBuffer { ptr, len: size }
    }
    
    // ✅ 安全公共接口
    pub fn read(&self, offset: usize) -> Option<u8> {
        if offset < self.len {
            Some(unsafe { *self.ptr.add(offset) })
        } else {
            None  // 安全:越界返回 None
        }
    }
    
    // ✅ 安全公共接口
    pub fn write(&mut self, offset: usize, value: u8) -> bool {
        if offset < self.len {
            unsafe { *self.ptr.add(offset) = value };
            true
        } else {
            false  // 安全:越界返回 false
        }
    }
}

impl Drop for SafeBuffer {
    fn drop(&mut self) {
        unsafe {
            alloc::alloc::dealloc(self.ptr, std::alloc::Layout::from_size_align(self.len, 1).unwrap());
        }
    }
}

6. MaybeUninit:未初始化内存

当你需要创建未初始化的内存时(如 C FFI 或性能优化),使用 MaybeUninit

use std::mem::MaybeUninit;

// ❌ 错误:未初始化的 Vec
let mut data: [u8; 1024] = [0; 1024];  // 初始化为 0

// ✅ 正确:使用 MaybeUninit
let mut data: [MaybeUninit<u8>; 1024] = MaybeUninit::uninit_array();

// 填充数据
for i in 0..1024 {
    data[i].write(i as u8);
}

// 安全地转换为初始化数组
let data: [u8; 1024] = unsafe {
    MaybeUninit::array_assume_init(data)
};

7. ManuallyDrop:阻止自动 Drop

当你想手动控制资源释放时:

use std::mem::ManuallyDrop;

let mut x = ManuallyDrop::new(Box::new(42));
println!("{}", *x);

// 手动释放
let boxed: Box<i32> = unsafe { ManuallyDrop::take(&mut x) };
// 现在 boxed 会正常 drop

8. 实现 Send 和 Sync

当你需要让自定义类型跨线程时:

use std::sync::Arc;

struct MyWrapper(*mut i32);

// ❌ 默认不是 Send/Sync
// 手动实现(需要确保线程安全)
unsafe impl Send for MyWrapper {}
unsafe impl Sync for MyWrapper {}

// ✅ 更安全的方式:使用 Arc
struct SafeWrapper(Arc<i32>);
// Arc 自动实现 Send 和 Sync

常见错误

错误 1: 不安全的 UTF-8 转换

let bytes = vec![0, 159, 146, 150];

// ❌ 假设字节是有效的 UTF-8
let s = unsafe {
    std::str::from_utf8_unchecked(&bytes)  // ⚠️ 如果无效就是未定义行为
};

// ✅ 安全版本
let s = std::str::from_utf8(&bytes).unwrap();  // 会检查

什么时候可以用 from_utf8_unchecked

仅在以下情况:

  • 你已经手动验证了字节是有效的 UTF-8
  • 性能关键路径且有基准测试证明瓶颈
  • 你有测试确保不会传入无效数据

错误 2: 悬垂指针

// ❌ 错误:返回悬垂指针
fn dangling_pointer() -> *const i32 {
    let x = 5;
    &x as *const i32  // x 在函数结束时被丢弃!
}

// ✅ 正确:返回拥有的值
fn safe_value() -> i32 {
    let x = 5;
    x  // 返回值,不是指针
}

错误 3: 数据竞争

use std::thread;

// ❌ 错误:多线程同时修改同一数据
let mut data = vec![1, 2, 3];
let ptr = data.as_mut_ptr();

let handles: Vec<_> = (0..3).map(|i| {
    thread::spawn(move || {
        unsafe {
            *ptr.add(i) += 1;  // 数据竞争!
        }
    })
}).collect();

// ✅ 正确:使用 Arc<Mutex<T>>
use std::sync::{Arc, Mutex};

let data = Arc::new(Mutex::new(vec![1, 2, 3]));

动手练习

练习 1: 安全的指针包装器

创建一个安全的指针包装器,防止空指针解引用:

// TODO: 实现 NonNullPtr<T> 结构体
// - 内部使用 *mut T
// - 提供安全的 new() 方法(拒绝空指针)
// - 提供安全的 get() 方法返回 &T
点击查看答案
use std::ptr::NonNull;

pub struct NonNullPtr<T> {
    ptr: NonNull<T>,
}

impl<T> NonNullPtr<T> {
    pub fn new(value: T) -> Self {
        let boxed = Box::new(value);
        let ptr = Box::into_raw(boxed);
        NonNullPtr {
            ptr: NonNull::new(ptr).unwrap(),  // 保证非空
        }
    }
    
    pub fn get(&self) -> &T {
        unsafe { self.ptr.as_ref() }
    }
}

impl<T> Drop for NonNullPtr<T> {
    fn drop(&mut self) {
        unsafe {
            drop(Box::from_raw(self.ptr.as_ptr()));
        }
    }
}

故障排查

Q: unsafe 真的不安全吗?

A: 不是。unsafe 意味着负责保证安全,而不是编译器。如果你正确使用了 unsafe,代码是安全的。

Q: 如何调试 unsafe 代码?

A:

  1. 使用 Miri 工具检测未定义行为:cargo +nightly miri run
  2. 启用 AddressSanitizer:RUSTFLAGS="-Z sanitizer=address" cargo run
  3. 编写充分的单元测试
  4. 使用 #[deny(unsafe_op_in_unsafe_fn)] 强制显式 unsafe

Q: 标准库中有多少 unsafe 代码?

A: 约 10-15%。像 VecStringHashMap 这样的核心数据结构底层都使用 unsafe,但它们提供了安全的公共接口。

Q: 如何安全地实现自定义集合?

A: 遵循以下模式:

  1. 使用 MaybeUninit 管理未初始化内存
  2. Drop 中正确释放资源
  3. 提供安全的公共接口
  4. 编写充分的测试(包括边界情况)
  5. 使用 Miri 验证未定义行为

知识扩展(选学)

unsafe 在标准库中的应用

Vec 的 push 实现(简化版):

impl<T> Vec<T> {
    pub fn push(&mut self, value: T) {
        if self.len == self.cap {
            self.grow();  // 重新分配
        }
        
        unsafe {
            // 直接写入,跳过边界检查
            std::ptr::write(self.ptr.add(self.len), value);
        }
        self.len += 1;
    }
}

小结

核心原则

  1. unsafe 不是"随便用": 只在必要且可控时使用
  2. 封装 unsafe: 提供安全的接口
  3. 记录安全契约: 为什么 unsafe 是安全的
  4. 优先安全抽象: Rust 标准库已经提供了大部分需要的工具

关键术语

  • Raw Pointer: 原始指针
  • Unsafe Block: unsafe 块
  • Undefined Behavior (UB): 未定义行为
  • FFI: 外部函数接口
  • Safety Invariant: 安全不变量

术语表

English中文
Raw pointer原始指针
Unsafe blockunsafe 块
Undefined behavior未定义行为
Safety contract安全契约
FFI外部函数接口
Memory safety内存安全

完整示例:src/basic/pointer_sample.rs


知识检查

快速测验(答案在下方):

  1. 原始指针和引用有什么区别?

  2. 什么时候需要使用 unsafe

  3. *const T*mut T 的区别?

点击查看答案与解析
  1. 原始指针不遵循借用规则,可以为空或悬垂
  2. 解引用原始指针、调用 unsafe 函数、访问可变静态
  3. *const T 不可变,*mut T 可变

关键理解: unsafe 是强大但危险的工具,应谨慎使用并封装在安全接口中。

延伸阅读

学习完指针与不安全代码后,你可能还想了解:

选择建议:

继续学习

🔴 记住:unsafe 让你对编译器说"我知道我在做什么,相信我"。确保你真的知道!

日志记录 (Logger)

开篇故事

想象你在驾驶一辆汽车,仪表盘告诉你车速、油量、发动机状态。没有这些信息,你就像在盲开。Rust 程序的日志就是仪表盘 - 它告诉你程序正在发生什么,帮助你诊断问题。


本章适合谁

如果你已经能写基础 Rust 代码,现在想知道如何让程序"开口说话"(输出运行信息),本章适合你。日志是调试和监控的关键工具。


你会学到什么

  1. 使用 env_logger 配置日志
  2. 不同日志级别(info, debug, error, trace)
  3. 自定义日志格式
  4. 日志与随机数生成
  5. 实际应用中的日志模式

前置要求

学习本章前,你需要理解:


第一个例子

// src/basic/logger_sample.rs

use env_logger;
use log::{info, debug, trace};
use rand::random;

pub fn logger_print() {
    // 初始化日志器,设置级别为 Debug
    env_logger::Builder::new()
        .filter(None, log::LevelFilter::Debug)
        .init();

    let n = random::<i32>();
    info!("logger is info, random n: {}", n);

    let m = random::<char>();
    trace!("logger trace is {}", m);

    let x = 32;
    debug!("this is debug info. {}", x);
}

输出示例

[INFO] logger is info, random n: 42
[TRACE] logger trace is a
[DEBUG] this is debug info. 32
[INFO] add_one result is 33

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
日志框架logging 模块Log4j / SLF4J无标准log crate + 实现Rust 分离接口和实现
日志级别DEBUG/INFO/WARN/ERROR同样无标准trace/debug/info/warn/errorRust 多了 trace 级别
配置方式logging.basicConfig()配置文件无标准环境变量 RUST_LOGRust 用环境变量控制
初始化自动需配置手动必须调用 init()Rust 需显式初始化
性能影响有开销有开销有开销编译时过滤Rust 零开销(编译时)

核心差异: Python 自带日志,Java 需第三方库,Rust 分离接口(log)和实现(env_logger),编译时可过滤。


原理解析

1. 日志级别

Rust 日志有 5 个级别(从低到高):

trace!("最详细的调试信息");  // TRACE - 最详细
debug!("调试信息");          // DEBUG - 开发时使用
info!("一般信息");           // INFO - 用户可见
warn!("警告");               // WARN - 潜在问题
error!("错误");              // ERROR - 严重影响

2. 初始化日志器

fn main() {
    // 方式 1:简单初始化(使用环境变量控制级别)
    env_logger::init();
    
    // 方式 2:自定义配置
    env_logger::Builder::new()
        .filter(None, log::LevelFilter::Debug)  // 设置级别
        .format_timestamp(Some(env_logger::TimestampPrecision::Millis))
        .init();
}

3. 环境变量控制

运行程序时设置 RUST_LOG 环境变量:

# 只显示错误
RUST_LOG=error cargo run

# 显示调试信息
RUST_LOG=debug cargo run

# 对不同模块设置不同级别
RUST_LOG=my_crate=debug,other_crate=info cargo run

常见错误

错误 1: 忘记初始化

fn main() {
    info!("This won't show up!");  // ❌ 没有初始化
}

修复:必须先初始化

fn main() {
    env_logger::init();  // ✅
    info!("Now it works!");
}

错误 2: 忘记添加依赖

# Cargo.toml
[dependencies]
log = "0.4"
env_logger = "0.10"

错误 3: 日志级别设置错误

env_logger::Builder::new()
    .filter(None, log::LevelFilter::Error)  // ❌ 只显示错误
    .init();

debug!("This debug won't show");  // 被过滤了

修复:根据需求选择级别

.filter(None, log::LevelFilter::Debug)  // ✅ 显示 debug 及以上

动手练习

练习 1: 设置日志级别

// TODO: 设置日志级别为 Warn,观察输出变化
env_logger::Builder::new()
    .filter(None, /* TODO: 这里填什么? */)
    .init();
点击查看答案
.filter(None, log::LevelFilter::Warn)

结果:只有 warn!error! 会显示,info!debug! 被过滤。

练习 2: 格式化日志

// TODO: 使用 format! 宏创建自定义日志消息
let x = 42;
info!(/* TODO: 自定义格式 */);
点击查看答案
info!("计算结果:{}", x);

故障排查 (FAQ)

Q: 日志没有输出怎么办?

A: 检查三点:

  1. 是否调用了 env_logger::init()
  2. RUST_LOG 环境变量设置
  3. 日志级别是否正确
# 临时设置
RUST_LOG=debug cargo run

Q: 如何把日志输出到文件?

A: 使用 env_loggerwrite_style 配置:

use std::fs::File;

let file = File::create("app.log").unwrap();
env_logger::Builder::new()
    .target(env_logger::Target::Pipe(Box::new(file)))
    .init();

Q: 生产环境应该用什么级别?

A: 推荐:

  • 开发: debug
  • 测试: info
  • 生产: warnerror

小结

核心要点

  1. 5 个日志级别: trace → debug → info → warn → error
  2. 必须初始化: env_logger::init()Builder::new().init()
  3. 环境变量控制: RUST_LOG=debug cargo run
  4. 灵活配置: 可为不同模块设置不同级别
  5. 性能考虑: 生产环境使用较高日志级别(减少输出)

术语

  • Log level (日志级别): 日志的严重等级
  • Filter (过滤): 根据级别筛选日志
  • Formatter (格式化器): 日志输出格式

下一步


术语表

English中文
Logger日志器
Log level日志级别
Filter过滤器
Format格式化

完整源码src/basic/logger_sample.rs


💡 提示:好的日志就像飞机的黑匣子 - 平时看不见,出问题时能救命!


知识检查

快速测验(答案在下方):

  1. log crate 和 env_logger 的关系是什么?

  2. 日志级别有哪些?

  3. 如何设置日志级别?

点击查看答案与解析
  1. log 提供 API,env_logger 是具体实现
  2. error, warn, info, debug, trace
  3. 设置 RUST_LOG 环境变量

关键理解: 日志是调试和监控生产应用的重要工具。

延伸阅读

学习完日志记录后,你可能还想了解:

选择建议:

继续学习

前一章: 指针与不安全代码
下一章: 追踪 (Tracing)

相关章节:

返回: 基础入门

追踪 (Tracing)

开篇故事

想象你在玩一个复杂的桌游,每走一步都有人记录:"玩家 A 从起点移动到第 5 格"。如果游戏出错了,你可以回放整个游戏过程找出问题。Rust 的追踪 (tracing) 就是这样 - 它记录程序的每一步执行,帮助你理解异步代码的执行流程。


本章适合谁

如果你已经学习了基础日志,现在想深入理解异步程序的执行流程,本章适合你。追踪是现代 Rust 异步编程的必备工具。


你会学到什么

  1. tracing 与 log 的区别
  2. Span的概念和使用
  3. 异步函数追踪
  4. 自定义追踪事件
  5. 性能影响分析

前置要求

  • 日志记录 - 基础日志概念
  • 异步编程 - async/await 基础

第一个例子

// src/basic/tracing_sample.rs

use tracing::{debug, error, info, span, warn, Level};

// #[tracing::instrument] 自动为函数创建 span
#[tracing::instrument]
pub fn shave(yak: usize) -> Result<(), Box<dyn std::error::Error>> {
    debug!(excitement = "yay!", "hello! I'm gonna shave a yak.");
    
    if yak == 3 {
        warn!("could not locate yak!");
        return Err(io::Error::new(io::ErrorKind::Other, "shaving yak failed!").into());
    } else {
        debug!("yak shaved successfully");
    }
    Ok(())
}

pub fn shave_all(yaks: usize) -> usize {
    // 创建 span 记录执行过程
    let span = span!(Level::TRACE, "shaving_yaks", yaks = yaks);
    let _enter = span.enter();
    
    info!("shaving yaks");
    
    let mut yaks_shaved = 0;
    for yak in 1..=yaks {
        if shave(yak).is_ok() {
            yaks_shaved += 1;
        }
    }
    
    yaks_shaved
}

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
追踪框架无标准无标准无标准tracing crateRust 有专用追踪框架
Span 概念有时间跨度的追踪Rust 独创 span 概念
异步支持强(跨线程追踪)Rust 为异步设计
结构化日志structlog需要库需要库原生支持Rust 原生结构化
自动仪器化#[instrument] 属性Rust 自动追踪函数

核心差异: Python/Java/C++ 无标准追踪框架,Rust 的 tracing 为异步而生,支持 span 和自动仪器化。


原理解析

1. Span vs Event

Span - 表示一段时间(函数执行、请求处理):

use tracing::{span, Level};

let span = span!(Level::INFO, "my_span");
let _enter = span.enter();  // 进入 span

// 这段代码在 span 内执行
// span 结束时自动记录耗时

Event - 表示一个时间点的事件:

use tracing::info;

info!("用户登录");  // 事件,没有时间跨度

2. 自动仪器化

使用 #[tracing::instrument] 自动追踪函数:

#[tracing::instrument]
fn process_user(id: u64) -> User {
    // 自动创建 span,记录参数和返回值
}

// 输出类似:
// shave{id=42}: process_user: entering
// shave{id=42}: process_user: exiting

3. 自定义字段

// 在 span 中添加自定义字段
let span = span!(
    Level::INFO,
    "database_query",
    user_id = user.id,
    query_time = start_time.elapsed().as_millis()
);

4. 异步支持

#[tracing::instrument]
async fn fetch_data(url: &str) -> Result<Data, Error> {
    // async 函数也能自动追踪
    // span 会跟随任务在不同线程间移动
}

常见错误

错误 1: 忘记 enter

let span = span!(Level::INFO, "my_span");
// ❌ 没有调用 enter(),span 不会生效

修复

let span = span!(Level::INFO, "my_span");
let _enter = span.enter();  // ✅

错误 2: 忘记添加依赖

# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"  # 用于输出

错误 3: 在同步代码中使用异步追踪

#[tokio::main]
async fn main() {
    // ❌ 忘记初始化 subscriber
    my_async_function().await;
}

修复

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();  // ✅
    my_async_function().await;
}

动手练习

练习 1: 创建 span

// TODO: 为这个函数添加追踪
fn process_order(order_id: u64) -> Result<(), Error> {
    // TODO: 创建 span,记录 order_id
    let _span = /* TODO */;
    let _enter = _span.enter();
    
    // 处理订单...
    Ok(())
}
点击查看答案
let _span = span!(Level::INFO, "process_order", order_id = order_id);

故障排查 (FAQ)

Q: tracing 和 log 有什么区别?

A:

  • log: 简单的事件日志,没有时间概念
  • tracing: 支持 span,可以追踪执行流程,特别适合异步代码

Q: 性能影响大吗?

A:

  • 默认级别以上:几乎为零(<1%)
  • 详细级别:可能有 5-10% 开销
  • 生产建议: 使用 warnerror 级别

Q: 如何输出到文件?

A:

use tracing_subscriber::fmt;
use std::fs::File;

let file = File::create("app.log").unwrap();
fmt::fmt()
    .with_writer(file)
    .init();

小结

核心要点

  1. Span vs Event: Span 有时间跨度,Event 是时间点
  2. 自动仪器化: #[tracing::instrument] 减少样板代码
  3. 异步友好: span 可以跟随任务跨线程
  4. 性能影响: 生产环境使用较高级别
  5. 调试利器: 可视化程序执行流程

术语

  • Span: 表示一段时间的追踪
  • Event: 时间点的事件
  • Instrument: 自动添加追踪
  • Subscriber: 处理追踪输出的组件

下一步


术语表

English中文
Span跨度
Event事件
Instrument仪器化
Subscriber订阅器

完整源码src/basic/tracing_sample.rs


💡 提示:追踪让你像看慢动作回放一样理解异步代码!


知识检查

快速测验(答案在下方):

  1. tracinglog 的区别是什么?

  2. span 的作用是什么?

  3. 如何添加自定义字段到 span?

点击查看答案与解析
  1. tracing 支持结构化日志和 span,log 是简单文本
  2. span 表示一段时间内的操作,可嵌套
  3. #[instrument] 中添加参数或使用 Span::current().record()

关键理解: tracing 是现代 Rust 应用的首选日志框架。

延伸阅读

学习完追踪后,你可能还想了解:

选择建议:

继续学习

前一章: 日志记录
下一章: 可见性控制

相关章节:

返回: 基础入门

可见性 (Visibility)

开篇故事

想象你在一个公司工作。有些信息是公开的(公司公告板),有些是部门内部的(部门会议),有些是私密的(HR 档案)。Rust 的可见性控制就像公司的信息分级 - 它决定哪些代码和数据可以被谁访问。


本章适合谁

如果你已经学完了结构体和模块,现在想学习如何控制代码的访问权限,本章适合你。可见性是封装和信息隐藏的基础。


你会学到什么

  1. pub 关键字的使用
  2. 结构体字段可见性
  3. 模块间可见性
  4. 私有构造器模式
  5. 封装最佳实践

前置要求


第一个例子

// src/basic/visiable_sample.rs

// 私有的结构体,带有一个公有的字段
mod visiable_sample {
    // 公有的结构体,带有一个公有的字段
    pub struct OpenBox<T> {
        pub contents: T,
    }

    // 公有的结构体,带有一个私有的字段
    pub struct ClosedBox<T> {
        contents: T,  // 私有!
    }

    impl<T> ClosedBox<T> {
        // 公有的构造器
        pub fn new(contents: T) -> ClosedBox<T> {
            ClosedBox { contents }
        }

        // 私有的 get 方法
        #[allow(dead_code)]
        fn get_contents(&self) -> &T {
            &self.contents
        }
    }
}

fn main() {
    // 公有字段可以自由访问
    let open_box = visiable_sample::OpenBox {
        contents: "public information",
    };
    println!("The open box contains: {}", open_box.contents);

    // 私有字段必须通过公有方法访问
    let closed_box = visiable_sample::ClosedBox::new("secret data");
    // ❌ println!("Closed: {}", closed_box.contents);  // 编译错误!
}

输出

The open box contains: public information

Python/Java/C++ vs Rust 对比

如果你有其他语言经验,这个对比会帮助你快速理解:

概念PythonJavaC++Rust关键差异
默认可见性公开包内可见公开私有Rust 默认私有
公开关键字无需声明public无需声明pubRust 需显式公开
包级可见protectedpub(crate)Rust crate 内可见
字段可见性无控制private 字段无控制pub 或私有字段Rust 字段独立控制
封装强制无(靠约定)编译器检查无(靠约定)编译器强制Rust 编译时保证

核心差异: Python/C++ 无强制可见性控制,Java 用 public/private/protected,Rust 默认私有且有 pub(crate) 等限定可见性。


原理解析

1. 可见性级别

Rust 有 3 种主要可见性:

// 私有(默认)
struct PrivateStruct;  // 只在当前模块内可用

// 公有
pub struct PublicStruct;  // 任何地方都可用

// 限定公有
pub(crate) struct CratePublic;  // 只在当前 crate 内公有
pub(super) struct ParentPublic;  // 只在父模块内公有

2. 结构体字段可见性

pub struct User {
    pub name: String,      // 公有字段
    pub(crate) email: String,  // crate 内公有
    age: u32,              // 私有字段
}

规则

  • 如果结构体是公有的,字段也必须标注可见性
  • 私有字段只能在定义它的模块内访问

3. 私有构造器模式

pub struct Database {
    connection: String,  // 私有
}

impl Database {
    // 通过公有方法控制创建
    pub fn connect(url: &str) -> Result<Database, Error> {
        // 可以在这里验证 URL、处理错误
        Ok(Database {
            connection: url.to_string()
        })
    }
}

// 使用:
// let db = Database { connection: "..." };  // ❌ 字段私有
let db = Database::connect("postgres://localhost");  // ✅

常见错误

错误 1: 结构体公有但字段私有

pub struct Point {
    x: i32,  // ❌ 私有字段
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };  // ❌ 无法访问私有字段
}

修复

pub struct Point {
    pub x: i32,  // ✅
    pub y: i32,
}

错误 2: 忘记 pub

mod my_module {
    fn public_function() {}  // ❌ 实际是私有的
}

修复

mod my_module {
    pub fn public_function() {}  // ✅
}

错误 3: 过度使用 pub

// ❌ 所有字段都公有,破坏了封装
pub struct User {
    pub password: String,  // 不应该公开!
    pub internal_id: u64,  // 内部实现细节
}

修复

pub struct User {
    username: String,    // ✅ 私有
    password: String,    // ✅ 私有
}

impl User {
    pub fn get_username(&self) -> &str {  // ✅ 公有访问器
        &self.username
    }
}

动手练习

练习 1: 设计可见性

为这个银行账户结构设计可见性:

// TODO: 添加适当的 pub 修饰符
struct BankAccount {
    account_number: String,  // 应该公有吗?
    balance: f64,           // 应该私有吗?
    
    fn new(number: String) -> Self {
        BankAccount {
            account_number: number,
            balance: 0.0,
        }
    }
}
点击查看答案
pub struct BankAccount {
    pub account_number: String,  // 账户号可以公开
    balance: f64,               // 余额私有
}

impl BankAccount {
    pub fn new(number: String) -> Self {
        BankAccount {
            account_number: number,
            balance: 0.0,
        }
    }
    
    pub fn get_balance(&self) -> f64 {  // 提供公有访问器
        self.balance
    }
}

故障排查 (FAQ)

Q: 什么时候应该用私有字段?

A:

  • 私有:内部实现细节、敏感数据
  • 公有:用户需要直接访问的非敏感数据
  • 一般规则:默认私有,需要时再公开

Q: 私有方法有什么用处?

A:

pub struct Parser {
    data: String,
}

impl Parser {
    pub fn parse(&self) -> Result {
        self.validate()?;  // 内部校验
        self.transform()   // 内部转换
    }
    
    fn validate(&self) -> Result<()> {  // 私有
        // 实现细节,用户不需要知道
    }
    
    fn transform(&self) -> Result {  // 私有
        // 实现细节
    }
}

Q: pub(crate) 和 pub 有什么区别?

A:

  • pub: 整个程序(包括其他 crate)都可以访问
  • pub(crate): 只在当前 crate 内公有,外部不可见
  • 使用场景:库的内部 API 应该用 pub(crate)

小结

核心要点

  1. 默认私有: 模块、函数、字段默认都是私有的
  2. pub 控制: 使用 pub 关键字公开
  3. 封装优先: 默认私有,需要时再公开
  4. 私有构造器: 通过方法控制对象创建
  5. 信息隐藏: 隐藏实现细节,暴露稳定接口

术语

  • Visibility (可见性): 代码的访问权限
  • Encapsulation (封装): 隐藏实现细节
  • Public interface (公有接口): 对外暴露的方法
  • Private implementation (私有实现): 内部实现细节

下一步

  • 相关:模块
  • 进阶:封装模式

术语表

English中文
Visibility可见性
Encapsulation封装
Public公有
Private私有

完整源码src/basic/visiable_sample.rs


💡 提示:好的可见性设计让你的代码像精密的钟表 - 用户只需要看表盘,不需要知道齿轮怎么转!


知识检查

快速测验(答案在下方):

  1. Rust 中默认的可见性是什么?

  2. pubpub(crate)pub(super) 的区别?

  3. 如何重新导出名称?

点击查看答案与解析
  1. 默认私有(private)
  2. pub 公开,pub(crate) crate 内可见,pub(super) 父模块可见
  3. 使用 pub usepub use inner_module::MyType

关键理解: 可见性控制是封装和 API 设计的重要部分。

延伸阅读

学习完可见性控制后,你可能还想了解:

选择建议:

继续学习

前一章: 追踪 (Tracing)
下一章: 高级进阶 🎓

相关章节:

返回: 基础入门

阶段复习:基础部分

开篇故事

想象你刚学完驾驶理论——你知道交通规则、标志含义、操作步骤。但真正上路前,你需要一次综合练习:在模拟环境中把所有知识串联起来。阶段复习就是你的"驾驶模拟考"——把分散的概念整合成完整的能力。


本章适合谁

如果你已经完成了基础部分(第 1-10 章),现在想检验自己的学习成果,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 综合运用所有权、借用、生命周期知识
  2. 识别和修复常见的 Rust 编译错误
  3. 设计包含结构体、枚举、特征的系统
  4. 理解模块可见性和代码组织

前置要求

完成以下章节:


第一个例子

回顾所有权的核心模式:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // 移动
    // println!("{}", s1);  // ❌ 错误
    
    let s3 = s2.clone();  // 克隆
    println!("{}, {}", s2, s3);  // ✅ 两者都可用
}

这个简单的例子涵盖了 Rust 最核心的概念:所有权转移和克隆。


原理解析

知识整合图

基础部分知识体系:

变量与表达式 ──→ 数据类型 ──→ 所有权 ──→ 结构体
                                         ↓
模块系统 ←── 线程与并发 ←── 闭包 ←── 泛型 ←── 特征 ←── 枚举

每个概念都建立在前一个概念之上,形成完整的知识链。


复习范围

第 1-10 章:变量与表达式、数据类型、所有权、结构体、枚举、特征、泛型、闭包、模块、线程


综合练习:设计一个简单的游戏角色系统

练习 1:角色定义

// TODO: 定义 Character 结构体
// 字段:name (String), health (u32), level (u32), class (职业枚举)

// TODO: 定义 Class 枚举
// 变体:Warrior, Mage, Ranger

// TODO: 实现 Character::new() 构造函数
点击查看答案
#[derive(Debug)]
enum Class {
    Warrior,
    Mage,
    Ranger,
}

#[derive(Debug)]
struct Character {
    name: String,
    health: u32,
    level: u32,
    class: Class,
}

impl Character {
    fn new(name: &str, class: Class) -> Self {
        Character {
            name: name.to_string(),
            health: 100,
            level: 1,
            class,
        }
    }
}

练习 2:角色行为

// TODO: 实现 Character 的方法
// - level_up() - 等级 +1,生命值 +10
// - take_damage(amount: u32) - 减少生命值
// - is_alive() -> bool - 检查是否存活
// - display(&self) - 打印角色信息
点击查看答案
impl Character {
    fn level_up(&mut self) {
        self.level += 1;
        self.health += 10;
        println!("{} 升级到等级 {}!", self.name, self.level);
    }

    fn take_damage(&mut self, amount: u32) {
        if amount >= self.health {
            self.health = 0;
            println!("{} 被击败了!", self.name);
        } else {
            self.health -= amount;
            println!("{} 受到 {} 点伤害,剩余 {} 点生命值", 
                     self.name, amount, self.health);
        }
    }

    fn is_alive(&self) -> bool {
        self.health > 0
    }

    fn display(&self) {
        println!("{} (等级 {}, {:?}) - 生命值: {}", 
                 self.name, self.level, self.class, self.health);
    }
}

练习 3:战斗系统

// TODO: 实现 attack 函数
// 两个角色互相攻击
// Warrior: 造成 15-25 点伤害 (随机)
// Mage: 造成 20-30 点伤害,但自己受到 5 点反噬
// Ranger: 造成 10-20 点伤害,如果先手则 +10

// 提示:使用 rand crate 生成随机数
点击查看答案
use rand::Rng;

fn attack(attacker: &mut Character, defender: &mut Character) {
    if !attacker.is_alive() || !defender.is_alive() {
        return;
    }
    
    let mut rng = rand::thread_rng();
    let damage = match attacker.class {
        Class::Warrior => rng.gen_range(15..26),
        Class::Mage => {
            let dmg = rng.gen_range(20..31);
            attacker.take_damage(5);  // 反噬
            dmg
        }
        Class::Ranger => rng.gen_range(10..21),
    };
    
    println!("{} 攻击 {},造成 {} 点伤害!", 
             attacker.name, defender.name, damage);
    defender.take_damage(damage);
}

知识检查

问题 1:所有权转移

这段代码会编译通过吗?为什么?

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1);
}
点击查看答案

不会通过s1 的所有权已移动给 s2s1 不再有效。

修复方法

let s2 = s1.clone();  // 克隆
// 或
println!("{}", &s1);  // 先借用再移动

问题 2:借用规则

以下代码有什么问题?

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;
    println!("{}, {}, {}", r1, r2, r3);
}
点击查看答案

编译错误。不能同时存在不可变引用 (r1, r2) 和可变引用 (r3)。

修复方法

let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2);  // 不可变引用使用完毕
let r3 = &mut s;  // 现在可以创建可变引用

问题 3:特征实现

为以下类型实现 Display trait:

struct Point {
    x: f64,
    y: f64,
}
点击查看答案
use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

问题 4:闭包捕获

fn main() {
    let x = 4;
    let equal_to_x = |z| z == x;
    let y = 4;
    assert!(equal_to_x(y));
}

闭包 equal_to_x 如何捕获 x

点击查看答案

不可变借用。闭包通过 &x 捕获 x,因为只需要读取 x 的值。

如果想强制移动捕获:

let equal_to_x = move |z| z == x;

问题 5:模块可见性

mod outer {
    mod inner {
        pub fn hello() {
            println!("Hello!");
        }
    }
    
    pub fn call_hello() {
        inner::hello();
    }
}

fn main() {
    outer::call_hello();
    // outer::inner::hello();  // ❌ 为什么不行?
}
点击查看答案

inner 模块本身是私有的(没有 pub),所以外部无法访问 inner::hello(),即使 hellopub 的。

修复方法

pub mod inner {  // 模块也需要 pub
    pub fn hello() { ... }
}

常见错误回顾

错误原因修复
use after move所有权已转移使用 .clone() 或借用 &
cannot borrow as mutable违反借用规则确保同一时间只有一个可变引用
lifetime may not live long enough生命周期不匹配添加生命周期标注 'a
trait not implemented缺少 trait 实现使用 impl Trait for Type
private module模块可见性不足添加 pub 到模块声明

小结

核心要点

  1. 所有权是 Rust 内存安全的核心
  2. 借用规则防止数据竞争
  3. 特征实现多态
  4. 模块系统组织代码
  5. 复习是巩固知识的关键

关键术语

  • 所有权 (Ownership): 对值的独占访问
  • 借用 (Borrowing): 临时访问,不转移所有权
  • 特征 (Trait): 接口定义
  • 模块 (Module): 代码组织单元

术语表

English中文
Ownership所有权
Borrowing借用
Trait特征
Module模块
Visibility可见性
Closure闭包
Generic泛型

继续学习

💡 记住:复习是学习的重要部分。不要急于前进,确保每个概念都理解了!

高级进阶 (Advance)

开篇故事

想象你已经学会了 Rust 的基础:变量、所有权、结构体、枚举。现在你想建造一座真正的房子——不是玩具模型,而是能住人的。这就需要更强大的工具:电钻、锯子、水平仪。Rust 的高级特性就是你的"电动工具"——它们让复杂任务变得简单,让高性能代码成为可能。

本部分涵盖 Rust 生态系统的核心工具:异步编程、数据库操作、Web 开发、数据处理、系统编程、测试与模拟、宏编程。掌握这些,你就能构建生产级应用。


本章适合谁

如果你已经完成了 基础入门,现在想学习 Rust 在实际项目中的应用,本部分适合你。


你会学到什么

完成高级进阶后,你可以:

  1. 异步编程 - 使用 Tokio 编写高并发网络服务
  2. 数据库操作 - 使用 SQLx 和 Diesel 操作数据库
  3. Web 开发 - 使用 Axum 和 Hyper 构建 REST API
  4. 数据处理 - 序列化/反序列化 JSON、CSV 等格式
  5. 系统编程 - 文件操作、内存映射、进程管理
  6. 测试与模拟 - 编写单元测试和集成测试
  7. 宏编程 - 使用声明宏和过程宏减少代码重复

前置要求

  • 基础入门 全部章节
  • ✅ 理解所有权和借用
  • ✅ 理解结构体和特征
  • ✅ 基本的 Rust 项目结构知识

学习路径

阶段 1: 异步编程(核心)

异步编程
├── 异步编程基础 (async.md)
├── Futures 异步编程 (futures.md)
├── 并行计算 (rayon.md)
├── MIO 底层 I/O (mio.md)
└── 循环引用 (cyclerc.md)

为什么先学这个? 现代 Rust 服务几乎都是异步的。Tokio 是事实上的异步运行时。

阶段 2: 数据持久化

数据库操作
├── 数据库操作概览 (database/database.md)
├── SQLx 异步查询 (database/sqlx.md)
└── Diesel ORM (database/diesel.md)

为什么学这个? 几乎所有应用都需要存储和检索数据。

阶段 3: Web 服务

Web 开发
├── Axum Web 框架 (web/axum.md)
├── Hyper HTTP 底层 (web/hyper.md)
└── Ollama AI 集成 (web/ollama.md)

为什么学这个? 构建 API 和 Web 服务是 Rust 的主要应用场景。

阶段 4: 数据处理

数据处理
├── JSON 序列化 (data/json.md)
├── CSV 处理 (data/csv.md)
├── 零拷贝序列化 (data/rkyv.md)
└── 序列化基础 (data/serialization.md)

为什么学这个? 数据交换格式是系统间通信的基础。

阶段 5: 系统编程

系统编程
├── 文件与目录操作 (system/directory.md)
├── 临时文件 (system/tempfile.md)
├── 内存映射 (system/memmap.md)
├── 环境变量 (system/dotenv.md)
├── 字节处理 (system/bytes.md)
├── Cow 类型 (system/cow.md)
├── 进程管理 (system/process.md)
├── 系统信息 (system/sysinfo.md)
└── 资源嵌入 (system/includedir.md)

为什么学这个? 系统编程是 Rust 的核心优势。

阶段 6: 测试与模拟

测试与模拟
├── 测试基础 (testing/test.md)
├── 模拟测试 (testing/mock.md)
└── 测试框架 (testing/rspec.md)

为什么学这个? 可靠的代码需要可靠的测试。

阶段 7: 宏编程(独立)

宏编程
├── 声明宏和过程宏 (testing/macros.md)
└── 派生宏 (testing/getset.md)

为什么学这个? 宏让你成为"元程序员",编写生成代码的代码。


代码示例

本部分每个章节都配有可运行的示例代码:

src/advance/
├── async_sample.rs       - 异步编程示例
├── futures_sample.rs     - Futures 示例
├── rayon_sample.rs       - 并行计算示例
├── mio_sample.rs         - MIO 示例
├── cyclerc_sample.rs     - 循环检测示例
├── sqlx_sample.rs        - SQLx 示例
├── diesel_sample.rs      - Diesel 示例
├── axum_sample.rs        - Axum 示例
├── hyper_sample.rs       - Hyper 示例
├── json_sample.rs        - JSON 序列化示例
├── csv_sample.rs         - CSV 处理示例
├── rkyv_sample.rs        - 零拷贝序列化示例
├── bytes_sample.rs       - 字节处理示例
├── cow_sample.rs         - Cow 类型示例
├── dotenv_sample.rs      - 环境变量示例
├── memmap_sample.rs      - 内存映射示例
├── process_sample.rs     - 进程管理示例
├── sysinfo_sample.rs     - 系统信息示例
├── includedir_sample.rs  - 资源嵌入示例
├── mock_sample.rs        - 模拟测试示例
├── getset_sample.rs      - 派生宏示例
├── macros_sample.rs      - 宏编程示例
└── typealias_sample.rs   - 类型别名示例

运行示例:

# 编译并运行特定示例
cargo run --bin async_sample
cargo run --bin macros_sample

项目结构

hello-rust/
├── src/
│   ├── basic/            # 基础入门
│   ├── advance/          # 高级进阶(本部分)
│   └── bin/              # 可运行二进制
├── crates/
│   ├── awesome/          # 生产级框架
│   ├── leetcode/         # 算法题解
│   └── macros/           # 过程宏
└── docs/                 # 文档

下一步

完成高级进阶后,继续学习 实战精选 部分,你将学习:

  • 数据库高级应用
  • 微服务架构
  • 消息队列
  • 依赖注入
  • 插件系统

💡 提示:高级进阶是 Rust 从"玩具"到"工具"的关键一步!


继续学习

前一章: 基础入门
下一章: 实战精选

相关章节:

返回: 目录

智能指针 (Smart Pointers)

开篇故事

想象你去住酒店。普通钥匙只能打开一扇门,而且你必须随身携带。智能卡则不同——它不仅能开门,还能记录你进入的次数,甚至当最后一个人离开时自动断电。

Rust 的智能指针就像这种智能卡。它们不仅指向内存中的数据,还携带额外的元数据(如引用计数)或能力(如内部可变性)。它们是 Rust 高级编程的基石。


本章适合谁

如果你已经理解了所有权的概念,现在想处理更复杂的场景(如多所有权、图结构、并发共享),本章适合你。


你会学到什么

完成本章后,你可以:

  1. 理解智能指针与普通引用的区别
  2. 使用 Box<T> 进行堆分配和定义递归类型
  3. 使用 Rc<T> 实现多所有权
  4. 使用 RefCell<T> 实现内部可变性
  5. 使用 Arc<T> 在线程间安全共享数据
  6. 识别并解决引用循环导致的内存泄漏

前置要求


第一个例子

最简单的智能指针 Box<T>

fn main() {
    // 将 i32 分配到堆上
    let b = Box::new(5);
    println!("b = {}", b);
    // b 离开作用域时,堆内存被释放
}

发生了什么?

  • Box::new(5) 在堆上分配内存存储 5
  • b 拥有这块堆内存的所有权。
  • b 离开作用域,Boxdrop 方法被调用,释放堆内存。

原理解析

1. Box - 堆分配

Box<T> 是最简单的智能指针,它允许你将数据存储在上而不是上。

使用场景:

  • 当数据太大,不想在栈上复制时。
  • 当你拥有一个在编译时大小未知的类型,但又需要在需要固定大小的上下文中使用它时。
  • 定义递归类型时(这是 Box 最重要的用途)。

递归类型示例:

// ❌ 错误:编译器不知道 List 有多大
// enum List {
//     Cons(i32, List),
//     Nil,
// }

// ✅ 正确:使用 Box 指向下一个节点
enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}

2. Rc - 引用计数

Rc<T> (Reference Counting) 允许多个所有者指向同一份数据。每当有一个新的所有者,计数加 1;当一个所有者离开作用域,计数减 1。当计数为 0 时,数据被清理。

内存布局:

Rc<String> 结构:
┌──────────────────────────────┐
│ 指针 (指向控制块)             │
└──────────┬───────────────────┘
           │
           ▼
┌──────────────────────────────┐
│ 强引用计数: 3                │
│ 弱引用计数: 0                │
│ 数据: "Hello"                │
└──────────────────────────────┘

使用场景: 图形结构(如 DOM 树)、事件监听器列表。

use std::rc::Rc;

fn main() {
    let s = Rc::new(String::from("Hello"));
    
    // 克隆 Rc 增加引用计数
    let s1 = Rc::clone(&s);
    let s2 = Rc::clone(&s);
    
    println!("引用计数:{}", Rc::strong_count(&s)); // 3
    
    // s2 离开作用域,计数减 1
}

3. RefCell - 内部可变性

Rust 的借用规则通常要求在编译时确定可变性。但 RefCell<T> 允许你在运行时检查借用规则。这被称为内部可变性模式。

对比:

  • RefCell<T>: 运行时检查,单线程。
  • Mutex<T>: 运行时检查,多线程。
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);
    
    // 可变借用
    {
        let mut d = data.borrow_mut();
        *d += 1;
    } // 借用在这里结束
    
    // 不可变借用
    let d = data.borrow();
    println!("data: {}", *d); // 6
}

注意: 如果违反借用规则(如同时有两个可变借用),程序会 panic

4. Arc - 线程安全的 Rc

Arc<T> (Atomic Reference Counting) 是 Rc<T> 的原子版本,可以安全地在线程间共享。

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    
    let mut handles = vec![];
    
    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            println!("Thread {}: {:?}", i, data_clone);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

初学者常见困惑

💡 这是很多学习者第一次遇到智能指针时的困惑——你并不孤单!

困惑 1: "为什么有了引用还需要智能指针?"

解答: 引用 (&T) 只是借用,不拥有数据。智能指针拥有数据,并提供额外功能。

#![allow(unused)]
fn main() {
// 引用:不拥有数据
let s = String::from("hello");
let r = &s; // r 只是借用

// Box: 拥有数据
let b = Box::new(String::from("hello")); // b 拥有堆上的 String
}

困惑 2: "Rc 和 Arc 有什么区别?"

解答:

  • Rc: 非原子操作,不能跨线程发送 (!Send),性能稍高。
  • Arc: 原子操作,可以跨线程发送 (Send + Sync),性能稍低。

选择指南:

  • 单线程多所有权 → Rc
  • 多线程多所有权 → Arc

困惑 3: "RefCell 和 Mutex 有什么区别?"

解答:

  • RefCell: 运行时检查借用规则,锁定,只能单线程使用。
  • Mutex: 运行时检查借用规则,锁定,可以跨线程使用。

常见错误

错误 1: 引用循环导致内存泄漏

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
    // parent: Rc<Node>, // ❌ 如果加上这一行,会形成循环引用!
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });
    
    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
    
    // 如果 leaf 也有对 branch 的 Rc 引用,两者都不会被释放
}

修复方法: 使用 Weak<T> 打破循环。

#![allow(unused)]
fn main() {
use std::rc::Weak;

struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>, // ✅ 使用 Weak
    children: RefCell<Vec<Rc<Node>>>,
}
}

错误 2: 运行时借用冲突

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(42);
    
    let a = x.borrow_mut();
    let b = x.borrow_mut(); // ❌ Panic! 已经有一个可变借用了
    
    println!("{}, {}", a, b);
}

修复方法: 确保借用作用域不重叠。

#![allow(unused)]
fn main() {
let mut a = x.borrow_mut();
// a 在这里被使用
drop(a); // 显式释放借用
let b = x.borrow_mut(); // ✅ 现在可以了
}

动手练习

练习 1: 使用 Rc 共享数据

创建一个程序,让三个列表共享同一组数据:

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    
    // TODO: 创建 list1, list2, list3 都引用 data
    // TODO: 打印每个列表的引用计数
}
点击查看答案
use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    
    let list1 = Rc::clone(&data);
    let list2 = Rc::clone(&data);
    let list3 = Rc::clone(&data);
    
    println!("引用计数:{}", Rc::strong_count(&data)); // 4
}

练习 2: 使用 RefCell 修改不可变数据

use std::cell::RefCell;

struct Counter {
    count: RefCell<i32>,
}

impl Counter {
    fn new() -> Self {
        Counter {
            count: RefCell::new(0),
        }
    }
    
    // TODO: 实现 increment 方法,即使 self 是不可变引用也能修改 count
    fn increment(&self) {
        // *self.count.borrow_mut() += 1;
    }
}

fn main() {
    let counter = Counter::new();
    counter.increment();
    counter.increment();
    println!("Count: {}", *counter.count.borrow());
}
点击查看答案
#![allow(unused)]
fn main() {
impl Counter {
    fn increment(&self) {
        *self.count.borrow_mut() += 1;
    }
}
}

故障排查 (FAQ)

Q: 什么时候使用 Box,什么时候直接存值?

A:

  • 数据很大(如大结构体)→ Box
  • 需要多态(Trait Object)→ Box<dyn Trait>
  • 递归类型 → Box
  • 否则 → 直接存值(栈上更快)

Q: Rc<RefCell<T>> 是什么组合?

A: 这是 Rust 中非常常见的模式:

  • Rc: 提供多所有权。
  • RefCell: 提供内部可变性。
  • 组合起来:多个所有者都可以修改数据。

Q: 为什么 Rust 不自动处理循环引用?

A: Rust 没有垃圾回收器 (GC)。它依赖确定性的析构(Drop)。循环引用会导致引用计数永远不为 0,从而内存泄漏。这是无 GC 语言的权衡。


知识扩展

Weak 详解

Weak<T>Rc<T> 的非拥有版本。它不会增加强引用计数,因此不会阻止数据被清理。

#![allow(unused)]
fn main() {
let strong = Rc::new(42);
let weak = Rc::downgrade(&strong);

// 尝试升级 Weak 为 Rc
if let Some(strong_ref) = weak.upgrade() {
    println!("数据还在:{}", strong_ref);
} else {
    println!("数据已被清理");
}
}

小结

核心要点:

  1. 智能指针: 拥有数据并提供额外元数据或能力。
  2. Box: 堆分配,递归类型。
  3. Rc: 单线程多所有权。
  4. RefCell: 运行时借用检查(内部可变性)。
  5. Arc: 多线程多所有权。
  6. Weak: 打破循环引用。

关键术语:

  • Smart Pointer (智能指针): 拥有数据的指针。
  • Deref Trait: 允许智能指针像引用一样使用。
  • Interior Mutability (内部可变性): 即使数据是不可变的也能修改它。
  • Reference Cycle (引用循环): 导致内存泄漏的循环引用。

术语表

English中文
Smart Pointer智能指针
Reference Counting引用计数
Interior Mutability内部可变性
Reference Cycle引用循环
Weak Reference弱引用
Strong Reference强引用

延伸阅读

继续学习


知识检查

问题 1 🟢 (基础)

Box<T> 的主要用途是什么?

A) 多线程共享数据
B) 将数据分配到堆上
C) 运行时借用检查
D) 增加引用计数

点击查看答案

答案: B) 将数据分配到堆上

解析: Box 允许你将数据存储在堆上,并拥有一个指向该数据的指针。

问题 2 🟡 (中等)

以下代码会 Panic 吗?

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(5);
    let a = x.borrow();
    let b = x.borrow();
    println!("{}, {}", a, b);
}
点击查看答案

答案: 不会 Panic。

解析: RefCell 允许多个不可变借用同时存在。只有当存在可变借用时才会冲突。

问题 3 🔴 (困难)

为什么 Rc<T> 不能跨线程使用?

点击查看答案

答案: 因为 Rc 的引用计数操作不是原子的。

解析: 在多线程环境下,两个线程可能同时修改引用计数,导致数据竞争。应使用 Arc<T>

错误处理

开篇故事

想象你在经营一家餐厅。同步错误处理就像每道菜做错时就立即停下来纠正——顾客等着上菜,整个流程都阻塞了。而在 Rust 的异步世界中,错误处理更加优雅:你可以继续处理其他订单,同时标记出错的菜品,稍后统一处理。这就是 Rust 错误处理的核心思想:显式、安全、不 panic

在 Rust 中,错误分为两大类:可恢复的错误(使用 Result<T, E>)和不可恢复的错误(使用 panic!)。本章专注于可恢复的错误处理,这是生产代码中最常见的场景。


本章适合谁

如果你已经理解 Rust 的基础语法和所有权,现在想学习如何在实际项目中优雅地处理错误——特别是异步环境下的错误传播和转换,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 使用 Result<T, E>? 操作符进行错误传播
  2. 实现 From trait 自动转换错误类型
  3. 使用 Box<dyn Error> 简化错误处理
  4. 在异步函数中正确处理错误
  5. 避免 .unwrap() 在生产代码中的滥用

前置要求

学习本章前,你需要理解:

  • 所有权 - 理解所有权转移和借用
  • 特征 - 理解 trait 定义和实现
  • 泛型 - 理解泛型语法

依赖安装

运行以下命令安装所需依赖:

cargo add thiserror
cargo add anyhow

第一个例子

让我们看一个最简单的错误处理示例:

use std::fs::File;
use std::io::{self, Read};

fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;  // 如果失败,返回 io::Error
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_file("hello.txt") {
        Ok(text) => println!("文件内容:{}", text),
        Err(e) => println!("读取失败:{}", e),
    }
}

发生了什么?

第 5 行 File::open(path)? 使用 ? 操作符:如果打开成功返回 File,如果失败立即返回 io::Error。这比 match 更简洁。


原理解析

1. Result<T, E> 枚举

pub enum Result<T, E> {
    Ok(T),   // 成功,包含值
    Err(E),  // 失败,包含错误
}

常用方法

let result: Result<i32, &str> = Ok(42);

// 获取值(panic 如果 Err)
let value = result.unwrap();

// 获取值或默认值
let value = result.unwrap_or(0);

// 匹配处理
match result {
    Ok(v) => println!("成功:{}", v),
    Err(e) => println!("错误:{}", e),
}

2. ? 操作符

? 是错误传播的语法糖:

// 以下两种写法等价:

// 使用 ?
fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

// 使用 match
fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = match File::open(path) {
        Ok(f) => f,
        Err(e) => return Err(e),
    };
    let mut contents = String::new();
    match file.read_to_string(&mut contents) {
        Ok(_) => Ok(contents),
        Err(e) => return Err(e),
    }
}

关键点

  • ? 只能在返回 Result 的函数中使用
  • ? 自动将错误类型转换为函数返回的错误类型

3. From trait 错误转换

实现 From trait 可以自动转换错误类型:

use std::fs::File;
use std::io::{self, Read, Write};

// 自定义错误类型
#[derive(Debug)]
enum MyError {
    Io(io::Error),
    ParseError(String),
}

// 实现 From<io::Error>
impl From<io::Error> for MyError {
    fn from(err: io::Error) -> Self {
        MyError::Io(err)
    }
}

// 现在可以混合使用不同错误
fn process_file(path: &str) -> Result<String, MyError> {
    let mut file = File::open(path)?;  // 自动转换为 MyError::Io
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

4. Box 简化错误

当有多种错误来源时,使用 Box<dyn Error> 避免定义复杂的枚举:

use std::error::Error;
use std::fs::File;
use std::num::ParseIntError;

// 返回统一的错误类型
fn read_and_parse(path: &str) -> Result<i32, Box<dyn Error>> {
    let contents = File::open(path)
        .map_err(|e| e.to_string())?;
    
    let number: i32 = contents.parse()?;  // ParseIntError 自动转换
    
    Ok(number)
}

优点

  • 无需定义错误枚举
  • 任何实现 Error trait 的类型都可以返回

缺点

  • 运行时动态分发(轻微性能开销)
  • 调用者无法精确匹配错误类型

5. 异步环境中的错误处理

异步函数的错误处理与同步类似,但需要注意:

use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

async fn read_file_async(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path).await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    Ok(contents)
}

#[tokio::main]
async fn main() {
    match read_file_async("config.json").await {
        Ok(text) => println!("配置:{}", text),
        Err(e) => eprintln!("加载配置失败:{}", e),
    }
}

关键点

  • .await? 之前:File::open(path).await?
  • 异步错误传播与同步相同

常见错误

错误 1: 在生产代码中使用 .unwrap()

// ❌ 错误:panic 如果文件不存在
let file = File::open("config.json").unwrap();

// ✅ 正确:优雅处理错误
let file = match File::open("config.json") {
    Ok(f) => f,
    Err(e) => {
        eprintln!("无法打开配置文件:{}", e);
        return Err(e);
    }
};

// 或使用 ?
let file = File::open("config.json")?;

修复方法:生产代码永远不要使用 .unwrap(),使用 ?match

错误 2: 错误类型不匹配

use std::fs::File;
use std::num::ParseIntError;

// ❌ 错误:返回类型不匹配
fn read_number(path: &str) -> Result<i32, ParseIntError> {
    let contents = File::open(path)?;  // io::Error 无法转换为 ParseIntError
    contents.parse()
}

// ✅ 正确:使用 Box<dyn Error>
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let contents = std::fs::read_to_string(path)?;
    let number: i32 = contents.parse()?;
    Ok(number)
}

错误 3: 忽略错误

// ❌ 错误:忽略错误
let _ = File::create("important.log");

// ✅ 正确:至少记录错误
if let Err(e) = File::create("important.log") {
    eprintln!("创建日志文件失败:{}", e);
}

错误 4: 过度使用 Box

// ❌ 不必要:单一错误类型
fn simple_func() -> Result<i32, Box<dyn Error>> {
    Ok(42)
}

// ✅ 更好:使用具体类型
fn simple_func() -> Result<i32, std::io::Error> {
    Ok(42)
}

动手练习

练习 1: 实现 From trait

为以下自定义错误实现 From<io::Error>

use std::io;

#[derive(Debug)]
enum AppError {
    DatabaseError(String),
    IoError(io::Error),
    ConfigError(String),
}

// TODO: 实现 From<io::Error> for AppError

fn read_config(path: &str) -> Result<String, AppError> {
    let contents = std::fs::read_to_string(path)?;
    Ok(contents)
}
点击查看答案
impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        AppError::IoError(err)
    }
}

fn read_config(path: &str) -> Result<String, AppError> {
    let contents = std::fs::read_to_string(path)?;  // 自动转换
    Ok(contents)
}

解析: 实现 From 后,? 自动将 io::Error 转换为 AppError::IoError

练习 2: 使用 Box

重构以下函数使用 Box<dyn Error>

use std::fs::File;
use std::num::ParseIntError;

#[derive(Debug)]
enum CombinedError {
    Io(std::io::Error),
    Parse(ParseIntError),
}

fn read_id(path: &str) -> Result<i32, CombinedError> {
    let contents = File::open(path)
        .and_then(|mut f| {
            use std::io::Read;
            let mut s = String::new();
            f.read_to_string(&mut s)?;
            Ok(s)
        })?;
    
    let id: i32 = contents.trim().parse()?;
    Ok(id)
}
点击查看答案
use std::error::Error;

fn read_id(path: &str) -> Result<i32, Box<dyn Error>> {
    let contents = std::fs::read_to_string(path)?;
    let id: i32 = contents.trim().parse()?;
    Ok(id)
}

解析: 使用 Box<dyn Error> 后,无需定义 CombinedError 枚举,代码更简洁。

练习 3: 异步错误处理

完成以下异步函数:

use tokio::fs::File;
use tokio::io::AsyncReadExt;

// TODO: 实现异步读取并解析 JSON
async fn read_json_config(path: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
    // 提示:使用 tokio::fs::File 和 AsyncReadExt
}
点击查看答案
use tokio::fs::File;
use tokio::io::AsyncReadExt;

async fn read_json_config(path: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
    let mut file = File::open(path).await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    Ok(contents)
}

解析: 异步错误处理与同步相同,只需添加 .await


故障排查

Q: 什么时候使用 .unwrap()?

A: 仅在以下场景:

  • 示例代码和原型
  • 测试代码
  • 确定不会失败的场景(如 "hello".len()

生产代码使用 ? 或适当的错误处理。

Q: Box 的性能开销有多大?

A: 动态分发有轻微开销(虚表查找),但对于 I/O 绑定操作可忽略。性能关键路径使用具体错误类型。

Q: 如何处理多个可能的错误类型?

A: 三种方案:

  1. 定义错误枚举 + 实现 From(推荐用于库)
  2. 使用 Box<dyn Error>(推荐用于应用)
  3. 使用 anyhow::Result(应用层最简洁)

Q: async 函数中错误类型需要 Send + Sync 吗?

A: 如果 Future 需要跨线程发送,需要 Box<dyn Error + Send + Sync>。单线程异步不需要。


知识扩展 (选学)

thiserror 库

thiserror 简化错误类型定义:

use thiserror::Error;

#[derive(Error, Debug)]
enum DataError {
    #[error("数据读取失败:{0}")]
    ReadFailed(#[from] std::io::Error),
    
    #[error("无效数据:{field}")]
    InvalidData { field: String },
}

anyhow 库

anyhow 简化应用层错误处理:

use anyhow::{Result, Context};

fn read_config() -> Result<String> {
    let contents = std::fs::read_to_string("config.json")
        .context("无法读取配置文件")?;
    Ok(contents)
}

小结

核心要点

  1. Result<T, E>: Rust 的错误处理类型,显式处理成功和失败
  2. ? 操作符: 简洁的错误传播语法
  3. From trait: 自动转换错误类型
  4. Box: 简化多种错误的返回
  5. 避免 .unwrap(): 生产代码使用优雅的错误处理

关键术语

  • panic: 不可恢复的错误,程序终止
  • Result: 可恢复错误的枚举类型
  • ? operator: 错误传播操作符
  • From trait: 类型转换 trait
  • Box: 动态错误类型

下一步

  • 学习 自定义错误类型
  • 实践 异步错误处理最佳实践
  • 回顾 Result 组合子

术语表

English中文
Error Handling错误处理
Panic恐慌
Result结果
Propagate传播
Unwrap解包
From TraitFrom 特征
Dynamic Dispatch动态分发
Type Erasure类型擦除

完整示例:src/advance/tokio_sample.rs - 异步错误处理模式
相关示例:src/advance/sqlx_sample.rs - 数据库错误处理


知识检查

快速测验(答案在下方):

  1. Result<T, E>Option<T> 有什么区别?

  2. ? 操作符做了什么?

  3. Box<dyn Error> 的优缺点是什么?

点击查看答案与解析
  1. Result 携带错误信息,Option 只有有/无
  2. 传播错误:如果是 Err 则返回,否则解包
  3. 优点 = 简单,缺点 = 动态分发、无法匹配具体错误类型

关键理解: 好的错误处理让程序更健壮。

继续学习

  • 下一步:数据库操作 - 实际项目中的错误处理
  • 进阶:Tokio 异步运行时 - 异步错误处理模式
  • 回顾:异步编程基础 - Result 在异步中的使用

💡 记住:好的错误处理让程序更健壮。永远不要忽略错误,显式处理每一个失败场景!

高级特征 (Advanced Traits)

开篇故事

想象你在设计一个通用的遥控器。基础遥控器只能开关电视。高级遥控器不仅能开关,还能根据电视型号自动调整频道,甚至能学习你的习惯。Rust 的特征系统也是如此——除了基础的方法定义,它还支持关联类型、默认泛型参数等高级功能,让你设计出更灵活、更强大的接口。


本章适合谁

如果你已经掌握了特征的基础用法,现在想深入理解标准库中的复杂特征(如 IteratorAdd),或者想解决同名方法冲突,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 使用关联类型 (Associated Types) 定义特征
  2. 使用默认泛型类型参数
  3. 使用完全限定语法 (Fully Qualified Syntax) 解决冲突
  4. 理解 Supertraits (特征继承)
  5. 使用 Newtype 模式实现外部类型的特征

前置要求


第一个例子

使用关联类型定义特征:

#![allow(unused)]
fn main() {
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
}

发生了什么?

  • type Item: 这是一个关联类型占位符。
  • 实现者必须指定 Item 的具体类型。
  • Self::Item: 使用关联类型。

原理解析

1. 关联类型 (Associated Types)

关联类型允许我们在特征定义中声明一个类型占位符,而不需要在使用特征时指定它。

对比泛型:

#![allow(unused)]
fn main() {
// 使用泛型
trait Container<T> {
    fn get(&self) -> &T;
}

// 使用关联类型
trait Container {
    type Item;
    fn get(&self) -> &Self::Item;
}
}

为什么使用关联类型:

  • 更简洁: 实现时不需要写 impl Container<i32> for MyType,只需 impl Container for MyType
  • 唯一性: 一个类型只能实现一次带有特定关联类型的特征,而泛型可以实现多次(不同参数)。

标准库示例: std::iter::Iterator

#![allow(unused)]
fn main() {
struct Counter {
    count: u32,
}

impl Iterator for Counter {
    type Item = u32; // 指定关联类型

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}
}

2. 默认泛型类型参数 (Default Generic Type Parameters)

我们可以为泛型参数指定默认值,通常用于运算符重载。

use std::ops::Add;

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

// Add trait 定义: trait Add<RHS = Self>
impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 3, y: 4 };
    assert_eq!(p1 + p2, Point { x: 4, y: 6 });
}

3. 完全限定语法 (Fully Qualified Syntax)

当多个特征或类型有同名方法时,编译器可能无法推断使用哪个。

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) { println!("Pilot flying"); }
}

impl Wizard for Human {
    fn fly(&self) { println!("Wizard flying"); }
}

impl Human {
    fn fly(&self) { println!("Human flying"); }
}

fn main() {
    let person = Human;
    person.fly(); // 调用 Human 的 fly (方法优先级高于 trait)
    
    // 调用特定 trait 的方法
    <Human as Pilot>::fly(&person);
    <Human as Wizard>::fly(&person);
}

4. Supertraits (特征继承)

Rust 没有传统的类继承,但可以通过 Supertraits 实现类似功能:一个特征要求实现者必须也实现另一个特征。

#![allow(unused)]
fn main() {
trait OutlinePrint: std::fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("* {} *", output);
        println!("{}", "*".repeat(len + 4));
    }
}
}

5. Newtype 模式 (Newtype Pattern)

为了在外部类型上实现外部特征(孤儿规则限制),我们可以使用 Newtype 模式:创建一个包含该类型的元组结构体。

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w); // w = [hello, world]
}

初学者常见困惑

💡 这是很多学习者第一次遇到高级特征时的困惑——你并不孤单!

困惑 1: "关联类型和泛型有什么区别?"

解答:

  • 泛型: 允许实现者为同一个类型实现特征多次(使用不同的类型参数)。
  • 关联类型: 一个类型只能实现特征一次,关联类型是固定的。
#![allow(unused)]
fn main() {
// 泛型:可以实现多次
impl Container<i32> for MyType { ... }
impl Container<String> for MyType { ... }

// 关联类型:只能实现一次
impl Container for MyType { type Item = i32; ... }
}

困惑 2: "为什么要用完全限定语法?"

解答: 当方法名冲突时。比如 Human 有自己的 fly 方法,同时也实现了 PilotWizardfly 方法。编译器默认调用类型自身的方法。要调用特征的,必须用 <Type as Trait>::method()


常见错误

错误 1: 孤儿规则 (Orphan Rule) 违规

// ❌ 错误:Vec 和 Display 都来自外部 crate
impl std::fmt::Display for Vec<String> { ... }

修复方法: 使用 Newtype 模式。

#![allow(unused)]
fn main() {
struct MyVec(Vec<String>);
impl std::fmt::Display for MyVec { ... }
}

动手练习

练习 1: 定义带关联类型的特征

定义一个 Mapper 特征,包含关联类型 Output 和方法 map

// TODO: 定义 Mapper trait

struct MyList;

// TODO: 为 MyList 实现 Mapper,Output 为 i32

fn main() {
    let list = MyList;
    // 使用 list.map(...)
}
点击查看答案
#![allow(unused)]
fn main() {
trait Mapper {
    type Output;
    fn map(&self, input: i32) -> Self::Output;
}

struct MyList;

impl Mapper for MyList {
    type Output = Vec<i32>;
    fn map(&self, input: i32) -> Self::Output {
        vec![input * 2]
    }
}
}

小结

核心要点:

  1. 关联类型: 简化特征实现,确保唯一性。
  2. 默认泛型参数: 用于运算符重载等场景。
  3. 完全限定语法: 解决同名方法冲突。
  4. Supertraits: 特征之间的依赖关系。
  5. Newtype 模式: 绕过孤儿规则。

术语表

English中文
Associated Type关联类型
Default Generic Parameter默认泛型参数
Fully Qualified Syntax完全限定语法
Supertrait超特征
Orphan Rule孤儿规则
Newtype Pattern新类型模式

延伸阅读

继续学习


知识检查

问题 1 🟢 (基础)

关联类型的主要优势是什么?

A) 允许一个类型实现特征多次
B) 简化实现语法,确保唯一性
C) 提高运行时性能
D) 允许运行时类型检查

点击查看答案

答案: B) 简化实现语法,确保唯一性

迭代器 (Iterators)

开篇故事

想象你在流水线上检查产品。传统方式是先拿到所有产品放在桌子上,然后一个一个检查。迭代器则不同——它像是一个传送带,产品在生产出来的同时就被检查,不需要一次性占用所有空间。

Rust 的迭代器不仅高效,而且提供了丰富的函数式编程接口(如 map, filter, fold),让你写出更简洁、更安全的代码。


本章适合谁

如果你已经掌握了基础的循环(for 循环),现在想学习更高级的数据处理方式,本章适合你。迭代器是 Rust 标准库中最强大的工具之一。


你会学到什么

完成本章后,你可以:

  1. 理解迭代器的惰性求值 (Lazy Evaluation) 特性
  2. 使用 Iterator trait 处理集合
  3. 使用适配器 (Adapters):map, filter, enumerate
  4. 使用消费者 (Consumers):collect, fold, sum
  5. 创建自定义迭代器

前置要求


第一个例子

使用迭代器处理向量:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // 使用迭代器处理数据
    let sum: i32 = numbers.iter()
        .filter(|&&x| x % 2 == 0)  // 只保留偶数
        .map(|&x| x * 2)           // 每个数乘以 2
        .sum();                    // 求和
        
    println!("结果:{}", sum); // 2*2 + 4*2 = 12
}

发生了什么?

  • iter(): 获取迭代器。
  • filter: 适配器,过滤元素。
  • map: 适配器,转换元素。
  • sum: 消费者,计算结果。

原理解析

1. 惰性求值 (Lazy Evaluation)

迭代器是惰性的。在你调用消费者方法之前,什么都不会发生。

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

// 只是创建了迭代器,没有执行任何操作
let iter = numbers.iter().map(|x| {
    println!("Processing {}", x);
    x * 2
});

// 调用 collect 时才会执行
let result: Vec<i32> = iter.collect();
}

输出:

Processing 1
Processing 2
Processing 3

2. 常用适配器 (Adapters)

适配器将迭代器转换为另一种迭代器。

map: 转换每个元素。

#![allow(unused)]
fn main() {
let nums = vec![1, 2, 3];
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
}

filter: 过滤元素。

#![allow(unused)]
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();
}

enumerate: 添加索引。

#![allow(unused)]
fn main() {
let names = vec!["Alice", "Bob"];
for (i, name) in names.iter().enumerate() {
    println!("{}: {}", i, name);
}
}

3. 常用消费者 (Consumers)

消费者消耗迭代器并产生结果。

collect: 收集到集合。

#![allow(unused)]
fn main() {
let nums = vec![1, 2, 3];
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
}

fold: 累积计算。

#![allow(unused)]
fn main() {
let nums = vec![1, 2, 3, 4];
let sum = nums.iter().fold(0, |acc, x| acc + x);
println!("Sum: {}", sum); // 10
}

find: 查找第一个匹配项。

#![allow(unused)]
fn main() {
let nums = vec![1, 2, 3, 4];
let first_even = nums.iter().find(|&&x| x % 2 == 0);
println!("{:?}", first_even); // Some(2)
}

4. 创建自定义迭代器

实现 Iterator trait 即可创建自定义迭代器。

struct Counter {
    count: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Self {
        Counter { count: 0, max }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let counter = Counter::new(5);
    let sum: u32 = counter.sum();
    println!("Sum: {}", sum); // 1+2+3+4+5 = 15
}

初学者常见困惑

💡 这是很多学习者第一次遇到迭代器时的困惑——你并不孤单!

困惑 1: "迭代器和循环有什么区别?"

解答:

  • 循环 (for): 命令式,关注"怎么做"。
  • 迭代器: 声明式,关注"做什么"。
  • 性能: 迭代器通常和循环一样快(甚至更快),因为编译器可以进行内联优化。

困惑 2: "为什么 iter() 返回的是引用?"

解答: iter() 返回元素的引用,避免移动数据。如果你想获取所有权,使用 into_iter()

#![allow(unused)]
fn main() {
let strings = vec![String::from("hi"), String::from("there")];

// iter(): 借用
for s in strings.iter() {
    println!("{}", s);
}
// strings 仍然可用

// into_iter(): 移动
for s in strings.into_iter() {
    println!("{}", s);
}
// strings 不再可用
}

常见错误

错误 1: 忘记调用消费者方法

let nums = vec![1, 2, 3];
nums.iter().map(|x| x * 2); // ❌ 警告:未使用的迭代器

// ✅ 正确:调用 collect
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();

错误 2: 类型推断失败

let nums = vec![1, 2, 3];
let result = nums.iter().sum(); // ❌ 错误:不知道 sum 的类型

// ✅ 正确:指定类型
let result: i32 = nums.iter().sum();

动手练习

练习 1: 过滤并转换

给定一个字符串列表,过滤出长度大于 3 的字符串,并将其转换为大写。

fn main() {
    let words = vec!["hi", "hello", "hey", "yo"];
    
    // TODO: 使用 filter 和 map 处理 words
    // 结果应该是 ["HELLO", "HEY"]
}
点击查看答案
#![allow(unused)]
fn main() {
let result: Vec<String> = words.iter()
    .filter(|w| w.len() > 3)
    .map(|w| w.to_uppercase())
    .collect();
}

练习 2: 使用 fold 计算阶乘

fn factorial(n: u32) -> u32 {
    // TODO: 使用 (1..=n).fold(...) 计算阶乘
}

fn main() {
    println!("5! = {}", factorial(5)); // 120
}
点击查看答案
#![allow(unused)]
fn main() {
fn factorial(n: u32) -> u32 {
    (1..=n).fold(1, |acc, x| acc * x)
}
}

小结

核心要点:

  1. 惰性求值: 迭代器在调用消费者前不执行。
  2. 适配器: map, filter, enumerate 等转换迭代器。
  3. 消费者: collect, fold, sum 等消耗迭代器产生结果。
  4. 自定义迭代器: 实现 Iterator trait。

关键术语:

  • Iterator (迭代器): 产生一系列值的对象。
  • Adapter (适配器): 转换迭代器的方法。
  • Consumer (消费者): 消耗迭代器的方法。
  • Lazy Evaluation (惰性求值): 延迟计算直到需要结果。

术语表

English中文
Iterator迭代器
Adapter适配器
Consumer消费者
Lazy Evaluation惰性求值
Closure闭包

延伸阅读

继续学习


知识检查

问题 1 🟢 (基础)

迭代器的主要优势是什么?

A) 代码更复杂
B) 惰性求值和丰富的函数式接口
C) 占用更多内存
D) 只能用于 Vec

点击查看答案

答案: B) 惰性求值和丰富的函数式接口

异步编程 (Async Programming)

开篇故事

想象你在餐厅点餐。同步编程就像你站在柜台前等待厨师做完每一道菜才点下一道——你干等着,什么也做不了。而异步编程就像你点完餐后拿到一个"取餐呼叫器"(Future),你可以去座位看书、玩手机,当菜好了呼叫器会通知你。这就是异步的核心思想:发起操作后不必等待完成,可以继续做其他事情

在 Rust 中,异步编程通过 Future trait 和 async/await 语法实现,让你能够编写高效的并发程序,同时保持代码的可读性。


本章适合谁

如果你已经理解 Rust 的基础所有权和生命周期,现在想学习如何编写高效的异步程序——比如同时处理多个网络请求、读写文件而不阻塞线程,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 解释什么是 Future 以及它如何工作
  2. 使用 async/await 语法编写异步函数
  3. 理解 poll 机制和执行器 (Executor) 的角色
  4. 使用组合器 (Combinators) 链接异步操作
  5. 区分 async 块和 async move 块的捕获行为

前置要求

学习本章前,你需要理解:

  • 所有权 - 理解所有权转移和借用
  • 生命周期 - 理解引用的有效范围
  • Trait - 理解 trait 和实现

依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add futures
cargo add tracing

第一个例子

让我们看一个最简单的异步示例:

use futures::executor::block_on;

async fn hello_world() {
    println!("hello, world!");
}

fn main() {
    let future = hello_world(); // 还没有打印!
    block_on(future); // 执行 Future,打印 "hello, world!"
}

发生了什么?

第 8 行 hello_world() 返回一个 Future,但此时并没有执行。异步函数返回的是一个"待执行的任务",就像餐厅里的取餐呼叫器——你拿到了它,但菜还没好。

第 9 行 block_on(future) 阻塞当前线程,直到 Future 完成。这就像你一直盯着呼叫器,直到它响。


原理解析

Future Trait 的核心

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),      // Future 已完成,返回结果
    Pending,       // Future 还未完成,需要再次 poll
}

Future 的执行流程:

+-------------------------------------------------------------+
|                    Future 生命周期                           |
+-------------------------------------------------------------+
|                                                              |
|   创建 Future --→ 首次 poll --→ Pending? --→ 等待事件        |
|        |              |            |                         |
|        |              v            v                         |
|        |           Ready? ←------ 事件就绪                   |
|        |              |                                      |
|        |              v                                      |
|        └--------→ 返回结果                                    |
|                                                              |
+-------------------------------------------------------------+

关键点:

  • Future 是惰性的——创建时不会执行,需要被 poll 才会推进
  • poll 方法可能返回 Pending(未完成)或 Ready(已完成)
  • 执行器 (Executor) 负责反复 poll 直到完成

async/await 语法

async fn 是创建 Future 的便捷方式:

// 这两种写法等价:

// async fn 语法糖
async fn foo() -> i32 {
    42
}

// 手动实现 Future(编译器展开后类似这样)
fn foo() -> impl Future<Output = i32> {
    async {
        42
    }
}

.await 是异步等待的关键:

async fn learn_and_sing() {
    // .await 会暂停当前 Future,让出线程执行其他任务
    let song = learn_song().await;  // 学歌时可以做其他事
    sing_song(song).await;          // 唱完再继续
}

执行流程可视化:

同步执行 (阻塞):                    异步执行 (非阻塞):
+---------+                       +---------+
| 学歌()   | ←-- 线程被占用          | 学歌()   | ←-- 开始学歌
| (等待)   |    什么也做不了         | .await  | ←-- 挂起,做其他事
|         |                       | 跳舞()   | ←-- 同时跳舞
| 唱歌()   | ←-- 学完后才能唱        | (等待)   |
+---------+                       |         |    学歌完成,继续唱
                                  | 唱歌()   |
                                  +---------+

常见错误

错误 1: 忘记 await

async fn fetch_data() -> String {
    "data".to_string()
}

async fn main() {
    let data = fetch_data();  // ❌ 没有 .await!
    println!("{}", data);     // 打印的是 Future,不是 String!
}

编译器输出:

error[E0277]: `impl Future<Output = String>` doesn't implement `Display`
  --> src/main.rs:7:20
   |
7  |     println!("{}", data);
   |                    ^^^^ `impl Future<Output = String>` cannot be formatted

修复方法

let data = fetch_data().await;  // ✅ 等待 Future 完成

错误 2: async 块捕获变量

async fn blocks() {
    let my_string = "foo".to_string();

    let future_one = async {
        println!("{}", my_string);  // ✅ 借用 my_string
    };

    let future_two = async {
        println!("{}", my_string);  // ✅ 也可以借用
    };

    futures::join!(future_one, future_two);  // ✅ 两者都能执行
}

但 async move 不同:

fn move_block() -> impl Future<Output = ()> {
    let my_string = "foo".to_string();
    async move {
        // my_string 被 move 进 Future
        println!("{}", my_string);
    }
    // ❌ my_string 不能再在这里使用!
}

区别

  • async {} - 按引用捕获变量,多个 async 块可访问同一变量
  • async move {} - 按值(move)捕获变量,只有一个能访问,但 Future 可以超出原作用域

错误 3: 在 async 中使用阻塞操作

async fn bad_example() {
    // ❌ 不要这样做!会阻塞整个线程
    std::thread::sleep(std::time::Duration::from_secs(1));
}

async fn good_example() {
    // ✅ 使用异步版本的 sleep
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}

为什么错误

  • thread::sleep 会让整个线程休眠,其他 Future 无法执行
  • 异步 sleep 只让当前 Future 挂起,线程可以去执行其他任务

动手练习

练习 1: 理解 Future 的惰性

下面的代码会打印什么?顺序是怎样的?

async fn learn_song() {
    println!("Learning song...");
}

async fn sing_song() {
    println!("Singing song...");
}

fn main() {
    let future1 = learn_song();  // 第1行
    let future2 = sing_song();   // 第2行
    println!("Created futures"); // 第3行
    
    block_on(future1);           // 第4行
    block_on(future2);           // 第5行
}
点击查看答案

输出顺序

Created futures
Learning song...
Singing song...

解析

  • 第1-2行只是创建 Future,没有执行,所以不会打印
  • 第3行立即执行,打印 "Created futures"
  • 第4行执行 future1,打印 "Learning song..."
  • 第5行执行 future2,打印 "Singing song..."

关键点:Future 是惰性的,必须被 poll 才会执行!


练习 2: 并发执行

如何让 learn_songdance 同时执行?

async fn learn_song() -> Song {
    /* 学歌需要时间 */
    Song
}

async fn dance() {
    /* 跳舞需要时间 */
}

async fn main() {
    // 当前是顺序执行,如何改成并发?
    let song = learn_song().await;
    dance().await;
}
点击查看答案

答案:使用 join!

async fn main() {
    // 同时开始两个 Future,等待两者都完成
    futures::join!(learn_song(), dance());
}

或者使用 async 块:

async fn main() {
    let f1 = learn_song();  // 创建 Future(未执行)
    let f2 = dance();       // 创建 Future(未执行)
    
    // join! 会并发执行两者
    futures::join!(f1, f2);
}

执行流程:

顺序执行:                    并发执行 (join!):
时间 →                      时间 →
|   学歌                    |   学歌 + 跳舞 同时开始
|   (等待)                  |   (等待)
|   跳舞                    |   两者都完成
+--------                   +--------

练习 3: async 块 vs async move 块

下面代码能编译通过吗?为什么?

fn example() {
    let data = vec![1, 2, 3];
    
    let f1 = async {
        println!("{:?}", data);
    };
    
    let f2 = async {
        println!("{:?}", data);
    };
    
    // 尝试执行两个 Future
    block_on(async {
        futures::join!(f1, f2);
    });
}

如果改成 async move 会怎样?

点击查看答案

当前代码:✅ 可以编译通过

  • async {} 按引用捕获 data
  • 两个 Future 都可以借用 data
  • data 的生命周期足够长(在 example 函数结束前不会 drop)

改成 async move

let f1 = async move {
    println!("{:?}", data);  // data 被 move 进 f1
};

let f2 = async move {
    println!("{:?}", data);  // ❌ 编译错误!data 已经被 move 到 f1 了
};

错误信息

error[E0382]: use of moved value: `data`

使用场景对比

  • async {} - 当你需要在多个 async 块中访问同一数据
  • async move {} - 当 Future 需要超出当前作用域,或你确定只有一个 Future 需要该数据

初学者常见困惑

💡 这是很多学习者第一次遇到异步编程时的困惑——你并不孤单!

困惑 1: "async/await 看起来像同步代码,为什么说是异步?"

解答: async/await 是语法糖,让异步代码看起来像同步代码。实际上:

你写的代码:
async fn fetch_data() {
    let data = fetch().await;  // 看起来像等待
    println!("{}", data);
}

实际发生的:
1. 调用 fetch() → 返回 Future
2. .await → 注册回调,释放线程
3. 当数据就绪时,回调被触发,继续执行
4. 线程在等待期间可以处理其他任务

关键区别:

  • 同步等待: 线程阻塞,什么都不做
  • 异步等待: 线程释放,处理其他任务

困惑 2: "Future 到底是什么?"

解答: Future 是一个状态机,表示"将来可能完成的计算":

Future 状态转换:

Pending (等待中) ──────→ Ready (已完成)
     │                       │
     │ 数据未就绪             │ 数据就绪
     │ 释放线程               │ 返回结果
     ▼                       ▼
 处理其他任务              继续执行

困惑 3: "为什么需要 Tokio?不能用标准库吗?"

解答: Rust 标准库只提供 Future trait,但不提供执行器

组件标准库Tokio
Future trait✅ 提供✅ 使用
Executor❌ 不提供✅ 提供
Reactor❌ 不提供✅ 提供
定时器❌ 不提供✅ 提供
网络 I/O❌ 不提供✅ 提供

类比:

  • Future = 菜谱(告诉你要做什么)
  • Executor = 厨师(实际执行)
  • Reactor = 厨房设备(I/O 事件通知)

困惑 4: ".await 到底做了什么?"

解答: .await 做三件事:

let result = future.await;

相当于:
1. 检查 future 是否就绪
2. 如果未就绪 → 保存状态,释放线程
3. 当就绪时 → 恢复状态,继续执行

困惑 5: "async fn 和普通 fn 有什么区别?"

解答:

#![allow(unused)]
fn main() {
// 普通函数:立即执行
fn normal_fn() -> i32 {
    42  // 立即返回
}

// 异步函数:返回 Future,需要 .await 执行
async fn async_fn() -> i32 {
    42  // 返回 Future<Output = i32>
}

// 调用:
let x = normal_fn();        // x = 42
let future = async_fn();    // future = Future (未执行)
let x = async_fn().await;   // x = 42 (执行后)
}

故障排查 (FAQ)

Q: 为什么我的 async 函数返回的不是实际值?

A: async 函数返回的是 Future,需要用 .await 获取结果:

async fn get_number() -> i32 {
    42
}

// ❌ 错误
let n: i32 = get_number();  // 实际上类型是 impl Future<Output = i32>

// ✅ 正确
let n: i32 = get_number().await;  // await 后得到 i32

Q: 如何选择使用 async fn 还是 async 块?

A:

场景推荐方式原因
可复用的异步逻辑async fn清晰、可复用
临时的异步代码async {}内联、简洁
需要捕获外部变量async {}async move {}灵活控制捕获方式
需要返回 Future 类型async fn 或返回 impl Future类型签名清晰

Q: await 和 block_on 有什么区别?

A:

特性.awaitblock_on
是否阻塞线程否,只阻塞当前任务是,阻塞整个线程
使用场景async 函数内部同步代码中启动异步
能否并发能,让出线程给其他任务不能,独占线程
示例let x = foo().await;block_on(foo())

最佳实践

  • 在 async 函数内部总是用 .await
  • main 函数或测试中用 block_on 进入异步世界

Q: 如何调试异步代码?

A:

  1. 添加日志追踪

    async fn my_function() {
        println!("Starting my_function");
        let result = some_async_op().await;
        println!("Got result: {:?}", result);
    }
  2. 使用 tracing 库(生产环境推荐):

    use tracing::{info, instrument};
    
    #[instrument]
    async fn my_function() {
        info!("Starting");
        let result = some_async_op().await;
        info!(result = ?result, "Completed");
    }

知识扩展 (选学)

Future 组合器

futures crate 提供了丰富的组合器:

use futures::future::{FutureExt, TryFutureExt};

// map - 转换结果
let future = fetch_data().map(|data| data.len());

// then - 链式调用
let future = fetch_user().then(|user| fetch_orders(user.id));

// join - 等待多个 Future
let (user, orders) = futures::join!(fetch_user(), fetch_orders());

// select - 等待任一 Future
futures::select! {
    user = fetch_user().fuse() => println!("Got user"),
    timeout = sleep(Duration::from_secs(5)).fuse() => println!("Timeout!"),
}

Pin 和 Unpin

当 Future 自引用时需要 Pin

use std::pin::Pin;

// async 块可能包含自引用,所以需要 Pin
fn poll_future(fut: Pin<&mut dyn Future<Output = ()>>) {
    // ...
}

大部分情况下你不需要关心 Pin,但理解它有助于调试复杂异步代码。


小结

核心要点

  1. Future 是惰性的 - 创建时不会执行,需要被 poll
  2. async fn 返回 Future - 需要 .awaitblock_on 来执行
  3. await 不阻塞线程 - 只阻塞当前任务,让出线程执行其他任务
  4. async vs async move - 前者按引用捕获,后者按值 move 捕获
  5. join! 实现并发 - 同时执行多个 Future,等待全部完成

关键术语

English中文说明
Future未来值代表异步计算的结果
async/await异步/等待Rust 的异步语法
Poll轮询推进 Future 执行的方法
Executor执行器管理和执行 Future 的运行时
Combinator组合器组合和转换 Future 的工具
Join合并并发执行多个 Future
Pending等待中Future 还未完成的状态
Ready就绪Future 已完成的状态

下一步

  • 学习 Tokio 运行时 - 最流行的 Rust 异步运行时
  • 理解 并发模式 - async 与多线程的结合
  • 探索 Stream - 异步版本的 Iterator

术语表

English中文
Future未来值
Async异步
Await等待
Poll轮询
Pending等待中
Ready就绪
Executor执行器
Task任务
Blocking阻塞
Non-blocking非阻塞
Combinator组合器
Concurrent并发
Parallel并行

完整示例:src/advance/futures_sample.rs


继续学习

💡 记住:异步编程的核心是"等待时不浪费资源"。当你需要等待 I/O(网络、文件、数据库)时,async 能让你的程序更高效地利用资源!


异步执行流程可视化

1. Future 状态机

                    +-------------+
         +---------→|  Not Started |←--------+
         |          |   (未开始)    |         |
         |          +------+------+         |
         |                 | Poll            |
         |                 v                |
         |          +-------------+         |
         |    +-----|   Pending   |-----+   |
         |    |     |  (等待中)    |     |   |
         |    |     +------+------+     |   |
         |    |            |            |   |
    Waker|    |  Poll      |      Poll  |   |Executor
   通知   |    |  未完成    |      未完成  |   |调度
         |    +------------+------------+   |
         |                   |              |
         |                   | Poll         |
         |                   | 完成          |
         |                   v              |
         |            +-------------+       |
         +------------|    Ready    |-------+
                      |   (已完成)   |
                      +-------------+
                            |
                            v
                      返回最终结果

2. 并发 vs 并行

单线程并发 (Async):          多线程并行:
+-----------------+          +-----------------+
| 线程 1          |          | 线程 1    线程 2 |
| +---+---+---+  |          | +---+    +---+  |
| | A | B | A |  |          | | A |    | B |  |
| +---+---+---+  |          | +---+    +---+  |
| (任务切换)      |          | (同时执行)      |
+-----------------+          +-----------------+

A: Task A 执行    A: Task A 执行
B: Task B 执行    B: Task B 执行

区别

  • 并发 (Concurrent) - 多个任务交替执行,提高资源利用率
  • 并行 (Parallel) - 多个任务同时执行,需要多核 CPU

Rust async 主要解决并发问题,让单线程能高效处理多个 I/O 任务。

3. 异步函数调用链

main()
  |
  +-→ block_on(async_main())
        |
        +-→ learn_and_sing().await
        |     |
        |     +-→ learn_song().await
        |     |       |
        |     |       +-→ 执行学歌...
        |     |       +-→ 返回 Song
        |     |
        |     +-→ sing_song(song).await
        |             |
        |             +-→ 执行唱歌...
        |
        +-→ dance().await
              |
              +-→ 执行跳舞...


实际并发执行 (使用 join!):

main()
  |
  +-→ block_on(async_main())
        |
        +-→ join!(learn_and_sing(), dance())
              |                    |
              +-→ 学歌 ------------+
              |    .await          +-→ 跳舞
              +-→ 唱歌              |   .await
                   .await           |
                                    |
              ←-- 两者都完成 -------+---+

知识检查

问题 1 🟢 (基础概念)

下面代码的输出顺序是什么?

async fn task1() { println!("1"); }
async fn task2() { println!("2"); }

fn main() {
    let f1 = task1();
    let f2 = task2();
    println!("start");
    block_on(f1);
    println!("middle");
    block_on(f2);
    println!("end");
}

A) start, 1, 2, middle, end
B) start, middle, end
C) start, 1, middle, 2, end
D) 1, 2, start, middle, end

答案与解析

答案: C) start, 1, middle, 2, end

解析

  1. let f1 = task1() 只是创建 Future,不会打印 "1"
  2. let f2 = task2() 只是创建 Future,不会打印 "2"
  3. 立即打印 "start"
  4. block_on(f1) 执行 Future,打印 "1"
  5. 打印 "middle"
  6. block_on(f2) 执行 Future,打印 "2"
  7. 打印 "end"

关键点:Future 是惰性的,创建时不执行!

问题 2 🟡 (并发执行)

如何让 task1task2 并发执行,并等待两者都完成?

async fn task1() { 
    println!("Task 1 start");
    // 模拟耗时操作
    println!("Task 1 done"); 
}

async fn task2() { 
    println!("Task 2 start");
    // 模拟耗时操作  
    println!("Task 2 done");
}

async fn main() {
    // 当前是顺序执行,如何改成并发?
    task1().await;
    task2().await;
}
答案与解析

答案:使用 futures::join!

async fn main() {
    futures::join!(task1(), task2());
}

可能的输出顺序

Task 1 start
Task 2 start
Task 1 done
Task 2 done

或:

Task 2 start
Task 1 start
Task 2 done
Task 1 done

解析

  • join! 会同时开始两个 Future
  • 执行顺序取决于调度器,但两者会并发执行
  • 只有当两者都完成后,main 才会继续

对比

顺序执行 (.await):           并发执行 (join!):
task1() --→ task2()         task1()
   |                           |
   v                        task2()
task1完成                      |
   |                           v
   v                        两者完成
task2开始
   |
   v
task2完成

问题 3 🔴 (所有权与 async)

下面代码能编译通过吗?如果不能,如何修复?

fn create_futures() -> (impl Future<Output = ()>, impl Future<Output = ()>) {
    let data = String::from("shared");
    
    let f1 = async move {
        println!("{}", data);
    };
    
    let f2 = async move {
        println!("{}", data);  // 能访问 data 吗?
    };
    
    (f1, f2)
}
答案与解析

答案:❌ 不能编译通过

错误信息

error[E0382]: use of moved value: `data`

原因

  • async move 会 move 捕获变量
  • data 在第一个 async move 块中已经被 move
  • 第二个 async move 块无法再使用 data

修复方法 1 - 使用 Arc 共享所有权

use std::sync::Arc;

fn create_futures() -> (impl Future<Output = ()>, impl Future<Output = ()>) {
    let data = Arc::new(String::from("shared"));
    
    let f1 = async move {
        println!("{}", data);  // Arc clone
    };
    
    let data2 = Arc::clone(&data);
    let f2 = async move {
        println!("{}", data2);  // 使用 clone
    };
    
    (f1, f2)
}

修复方法 2 - 使用 async (非 move)

fn create_futures() -> impl Future<Output = ()> {
    let data = String::from("shared");
    
    async {
        // 多个 async 块可以共享引用
        let f1 = async {
            println!("{}", data);
        };
        
        let f2 = async {
            println!("{}", data);
        };
        
        futures::join!(f1, f2);
    }
}

关键点

  • async move - 所有权转移,只能有一个 Future 拥有数据
  • async - 借用捕获,多个 Future 可以共享引用(但生命周期受限)

💡 记住:异步编程让你用顺序的代码风格编写高效的并发程序。理解 Future 的惰性本质和 await 的挂起机制是掌握 Rust async 的关键!

Tokio 异步运行时

开篇故事

想象你在经营一家快递公司。如果只有一个快递员(单线程),他必须按顺序送每个包裹:送完 A 才能送 B,送完 B 才能送 C。这很慢。

如果你雇了多个快递员(多线程),他们可以同时送包裹——但协调成本很高:谁送哪个?怎么避免重复?

Tokio 就像一位超级调度员:它管理一个快递员团队,当某个快递员等待客户开门时(I/O 阻塞),立刻安排他去送下一个包裹。这样每个快递员都在高效工作,不会浪费时间等待。这就是 Tokio 的核心思想:在等待 I/O 时做其他事情


本章适合谁

如果你想编写高并发网络服务,或者理解 Rust 异步编程的实际应用,本章适合你。Tokio 是 Rust 生态的事实标准异步运行时。


你会学到什么

完成本章后,你可以:

  1. 理解 Tokio 运行时的核心组件
  2. 使用 #[tokio::main] 启动异步程序
  3. 使用 tokio::spawn 创建异步任务
  4. 使用 mpsc 通道在任务间传递消息
  5. 使用 RwLock 安全共享状态
  6. 使用 spawn_blocking 运行阻塞代码
  7. 理解 oneshot 通道的单次通信模式

前置要求


第一个例子

最简单的 Tokio 服务器:

use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("服务器启动在 127.0.0.1:8080");

    loop {
        let (mut socket, _) = listener.accept().await?;
        
        tokio::spawn(async move {
            let mut buf = [0; 1024];
            loop {
                let n = socket.read(&mut buf).await.unwrap();
                if n == 0 {
                    return; // 客户端断开
                }
                socket.write_all(&buf[0..n]).await.unwrap();
            }
        });
    }
}

💡 注意:此代码需要客户端配合运行。请使用 cargo run --bin echo_server 和客户端程序进行完整测试。

发生了什么?

  • #[tokio::main] - 启动 Tokio 运行时
  • TcpListener::bind - 绑定端口
  • tokio::spawn - 为每个连接创建异步任务
  • 服务器可以并发处理多个客户端

原理解析

1. Tokio 运行时架构

Tokio 运行时
├── Reactor (I/O 多路复用)
│   ├── epoll (Linux)
│   ├── kqueue (macOS)
│   └── IOCP (Windows)
├── Scheduler (任务调度)
│   ├── 工作窃取 (work-stealing)
│   └── 多线程调度
└── Timer (定时器)
    └── 时间轮算法

2. 异步任务创建

// 方式 1: #[tokio::main]
#[tokio::main]
async fn main() {
    println!("主函数");
}

// 方式 2: 手动创建运行时
fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        println!("手动运行时");
    });
}

// 方式 3: tokio::spawn
#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        println!("子任务");
    });
    handle.await.unwrap();
}

3. 通道通信 (mpsc)

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    // 创建异步通道(缓冲区大小 100)
    let (tx, mut rx) = mpsc::channel(100);

    // 发送端任务
    let task = tokio::spawn(async move {
        let val = String::from("hello from tokio");
        tx.send(val).await.unwrap();
        drop(tx); // 重要!关闭发送端
    });

    // 接收端
    if let Some(received) = rx.recv().await {
        println!("收到:{}", received);
    }

    task.await.unwrap();
}

mpsc 特点

  • multiple producer, single consumer
  • 多发送端,单接收端
  • 异步非阻塞发送

4. 多任务并发

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(10);

    // 创建 50 个并发任务
    for i in 0..50 {
        let tx_clone = tx.clone();
        tokio::spawn(async move {
            let val = format!("task {}", i);
            tx_clone.send(val).await.unwrap();
        });
    }

    // 关闭发送端
    drop(tx);

    // 接收所有消息
    while let Some(received) = rx.recv().await {
        println!("收到:{}", received);
    }
}

5. 共享状态 (RwLock)

use tokio::sync::RwLock;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let data = Arc::new(RwLock::new(0));

    // 多个读任务
    let mut read_tasks = Vec::new();
    for _ in 0..5 {
        let data_clone = data.clone();
        read_tasks.push(tokio::spawn(async move {
            let read_guard = data_clone.read().await;
            println!("读取:{}", *read_guard);
        }));
    }

    // 一个写任务
    let data_clone = data.clone();
    let write_task = tokio::spawn(async move {
        let mut write_guard = data_clone.write().await;
        *write_guard += 1;
        println!("写入:{}", *write_guard);
    });

    // 等待所有任务
    for task in read_tasks {
        task.await.unwrap();
    }
    write_task.await.unwrap();
}

RwLock 特点

  • 允许多个读或一个写
  • 读锁不互斥,写锁独占
  • 适合读多写少场景

6. 阻塞代码 (spawn_blocking)

use parking_lot::RwLock;

#[tokio::main]
async fn main() {
    let data = Arc::new(parking_lot::RwLock::new(0));

    // 在阻塞线程池中运行
    let result = tokio::task::spawn_blocking({
        let data = data.clone();
        move || {
            let mut write_guard = data.write();
            *write_guard += 1;
            *write_guard
        }
    }).await.unwrap();

    println!("结果:{}", result);
}

为什么需要 spawn_blocking?

  • Tokio 是异步运行时,不适合 CPU 密集型或阻塞操作
  • spawn_blocking 将任务移到专用阻塞线程池
  • 避免阻塞异步运行时的主线程

7. Oneshot 通道

use tokio::sync::oneshot;

#[tokio::main]
async fn main() {
    let (tx, rx) = oneshot::channel();

    tokio::spawn(async move {
        tx.send("hello").unwrap();
    });

    let result = rx.await.unwrap();
    println!("收到:{}", result);
}

Oneshot 特点

  • 只能发送一次
  • 适合请求-响应模式
  • 比 mpsc 更轻量

常见错误

错误 1: 忘记 drop 发送端

// ❌ 错误:接收端会永远等待
let (tx, mut rx) = mpsc::channel(100);
tokio::spawn(async move {
    tx.send("hello").await.unwrap();
    // 忘记 drop(tx)
});

while let Some(msg) = rx.recv().await {
    println!("{}", msg);
} // 这里会死锁!

// ✅ 正确:关闭发送端
let (tx, mut rx) = mpsc::channel(100);
tokio::spawn(async move {
    tx.send("hello").await.unwrap();
    drop(tx); // 重要!
});

错误 2: 在异步上下文中使用阻塞操作

// ❌ 错误:阻塞异步运行时
#[tokio::main]
async fn main() {
    std::thread::sleep(Duration::from_secs(1)); // 阻塞!
}

// ✅ 正确:使用异步等待
#[tokio::main]
async fn main() {
    tokio::time::sleep(Duration::from_secs(1)).await;
}

错误 3: 共享随机数生成器

// ❌ 错误:thread_rng 不是 Send
tokio::spawn(async move {
    let mut rng = rand::thread_rng(); // 编译错误!
});

// ✅ 正确:每个任务创建自己的 rng
tokio::spawn(async move {
    let mut rng = StdRng::from_entropy();
    let num = rng.gen_range(0..100);
});

动手练习

练习 1: 实现 Echo 服务器

创建一个 TCP 服务器,回显客户端发送的消息:

// TODO: 实现 echo_server 函数
// 监听 127.0.0.1:8080
// 为每个连接创建异步任务
// 读取数据并原样返回
点击查看答案
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    
    loop {
        let (mut socket, _) = listener.accept().await?;
        
        tokio::spawn(async move {
            let mut buf = [0; 1024];
            loop {
                let n = socket.read(&mut buf).await.unwrap();
                if n == 0 {
                    return;
                }
                socket.write_all(&buf[0..n]).await.unwrap();
            }
        });
    }
}

💡 注意:此代码需要客户端配合运行。请使用 cargo run 启动服务器后,再使用客户端程序连接测试。


故障排查

Q: Tokio 和 async-std 有什么区别?

A:

  • Tokio: 功能更全,生态更大,性能更好
  • async-std: 标准库风格,更轻量
  • 推荐:生产环境用 Tokio

Q: 如何选择通道类型?

A:

  • mpsc - 多发送端,单接收端
  • oneshot - 单次通信
  • broadcast - 多发送端,多接收端
  • watch - 单发送端,多接收端,只保留最新值

Q: RwLock 和 Mutex 如何选择?

A:

  • RwLock - 读多写少
  • Mutex - 读写均衡或写多
  • Tokio 的 RwLock 是异步版本

知识扩展(选学)

Tokio 性能调优

# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }

运行时配置

#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
    // 4 个工作线程
}

小结

核心要点

  1. #[tokio::main]: 启动异步运行时
  2. tokio::spawn: 创建异步任务
  3. mpsc: 多发送端通道
  4. RwLock: 读写锁共享状态
  5. spawn_blocking: 运行阻塞代码

关键术语

  • Runtime: 运行时
  • Executor: 执行器
  • Reactor: I/O 反应器
  • Work-stealing: 工作窃取
  • Channel: 通道

术语表

English中文
Runtime运行时
Async Task异步任务
Channel通道
Work-stealing工作窃取
Blocking阻塞
Spawn生成任务

完整示例:src/advance/tokio_sample.rs


知识检查

快速测验(答案在下方):

  1. mpsc 通道的 mpsc 分别代表什么?

  2. 为什么发送端需要 drop(tx)

  3. RwLockMutex 如何选择?

点击查看答案与解析
  1. multiple producer, single consumer
  2. 不 drop 的话接收端会永远等待(认为还有更多消息)
  3. 读多写少用 RwLock,读写均衡用 Mutex

关键理解: Tokio 的通道是异步的,与 std::sync::mpsc 不同。

继续学习

💡 记住:Tokio 是 Rust 异步编程的基石。掌握它,你就能构建高并发服务!

Futures 异步编程

开篇故事

想象你在餐厅点餐。传统方式是:点餐 → 等待 → 取餐 → 吃。异步方式是:点餐 → 拿到号牌 → 继续做其他事 → 号牌响了去取餐。Futures 就像这个号牌——它代表一个将来会完成的任务。


本章适合谁

如果你已经了解了 async/await 基础,现在想深入理解 Future trait 和异步组合子,本章适合你。Futures 是 Rust 异步编程的核心抽象。


你会学到什么

完成本章后,你可以:

  1. 理解 Future trait 的工作原理
  2. 使用 block_on 执行 Future
  3. 链式组合多个 Future
  4. 并发执行多个任务
  5. 使用 join! 宏并发等待

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add futures

第一个例子

最简单的 Future 使用:

use futures::{executor::block_on, Future};

// 定义异步函数
async fn hello_world() {
    println!("hello, world!");
}

fn main() {
    // 创建 Future
    let future = hello_world();
    
    // 执行 Future
    block_on(future);  // 输出:hello, world!
}

完整示例: futures_sample.rs


原理解析

Future Trait

Future 是一个 trait

trait Future {
    type Output;
    
    // 轮询 Future 是否完成
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Poll 枚举

enum Poll<T> {
    Ready(T),      // 完成,返回结果
    Pending,       // 未完成,等待
}

block_on 执行器

最简单的执行器

use futures::executor::block_on;

async fn task1() -> i32 {
    42
}

async fn task2() -> String {
    "Hello".to_string()
}

fn main() {
    let result1 = block_on(task1());
    let result2 = block_on(task2());
    
    println!("{} {}", result1, result2);
}

Future 链式组合

使用 then 链式调用

use futures::{future, FutureExt};

let future = future::ready(42)
    .then(|x| async move { x * 2 })
    .then(|x| async move { x + 1 });

let result = block_on(future);
println!("{}", result);  // 85

并发执行

使用 join 并发等待

use futures::future::join;

async fn task1() -> i32 {
    42
}

async fn task2() -> String {
    "Hello".to_string()
}

fn main() {
    let (result1, result2) = block_on(join(task1(), task2()));
    println!("{} {}", result1, result2);
}

join! 宏

并发执行多个异步任务

async fn learn_song() -> Song {
    Song
}

async fn sing_song(song: Song) {
    println!("Singing: {:?}", song);
}

async fn dance() {
    println!("Dancing!");
}

async fn async_main() {
    // 使用 join! 并发执行
    futures::join!(
        learn_and_sing(),
        dance()
    );
}

async fn learn_and_sing() {
    let song = learn_song().await;
    sing_song(song).await;
}

常见错误

错误 1: Future 未执行

async fn task() {
    println!("Hello");
}

fn main() {
    task();  // ❌ 什么都不发生
    // Future 需要被 await 或 block_on
}

修复方法:

fn main() {
    block_on(task());  // ✅ 执行 Future
}

错误 2: 在同步上下文中 await

fn sync_function() {
    async { 42 }.await;  // ❌ await 只能在 async 函数中使用
}

修复方法:

async fn async_function() {
    async { 42 }.await;  // ✅
}

错误 3: 阻塞异步运行时

#[tokio::main]
async fn main() {
    // ❌ 这会阻塞整个运行时
    std::thread::sleep(Duration::from_secs(5));
}

修复方法:

#[tokio::main]
async fn main() {
    // ✅ 使用异步 sleep
    tokio::time::sleep(Duration::from_secs(5)).await;
}

动手练习

练习 1: 创建简单 Future

use futures::executor::block_on;

// TODO: 定义异步函数
// - 返回 i32
// - 返回 42

fn main() {
    // TODO: 执行 Future
    // TODO: 打印结果
}
点击查看答案
use futures::executor::block_on;

async fn get_answer() -> i32 {
    42
}

fn main() {
    let answer = block_on(get_answer());
    println!("答案是:{}", answer);
}

练习 2: 链式 Future

use futures::{future, FutureExt};

fn main() {
    // TODO: 创建 Future 链
    // 1. 从 ready(5) 开始
    // 2. 乘以 2
    // 3. 加 10
    // 4. 打印结果
}
点击查看答案
use futures::{future, FutureExt};

fn main() {
    let result = block_on(
        future::ready(5)
            .then(|x| async move { x * 2 })
            .then(|x| async move { x + 10 })
    );
    println!("{}", result);  // 20
}

练习 3: 并发执行

use futures::future::join;

async fn task1() -> i32 { 10 }
async fn task2() -> i32 { 20 }

fn main() {
    // TODO: 并发执行 task1 和 task2
    // TODO: 打印两个结果的和
}
点击查看答案
use futures::future::join;

fn main() {
    let (r1, r2) = block_on(join(task1(), task2()));
    println!("和:{}", r1 + r2);  // 30
}

故障排查 (FAQ)

Q: Future 和 async/await 有什么区别?

A:

  • Future: trait,表示异步计算
  • async/await: 语法糖,让 Future 更易使用
// 使用 async/await
async fn task() -> i32 { 42 }

// 底层是 Future
fn task() -> impl Future<Output = i32> {
    future::ready(42)
}

Q: 什么时候使用 block_on?

A:

  • block_on: 在同步上下文中执行 Future(如 main 函数)
  • .await: 在异步上下文中等待 Future
fn main() {
    block_on(async_main());  // 入口
}

async fn async_main() {
    task().await;  // 异步等待
}

Q: join 和 select 有什么区别?

A:

  • join: 等待所有 Future 完成
  • select: 等待第一个完成的 Future
// join - 都完成
let (r1, r2) = join(task1(), task2()).await;

// select - 第一个完成
let result = select(task1(), task2()).await;

知识扩展

select 使用

use futures::future::{select, Either};

async fn task1() -> i32 { 10 }
async fn task2() -> i32 { 20 }

fn main() {
    let result = block_on(async {
        match select(task1(), task2()).await {
            Either::Left((v, _)) => v,
            Either::Right((v, _)) => v,
        }
    });
    println!("{}", result);  // 10 或 20
}

Future 超时

use futures::{FutureExt, TryFutureExt};
use std::time::Duration;

async fn slow_task() -> i32 {
    tokio::time::sleep(Duration::from_secs(5)).await;
    42
}

fn main() {
    let result = block_on(
        slow_task()
            .timeout(Duration::from_secs(2))
            .unwrap_or(0)
    );
    println!("{}", result);  // 0 (超时)
}

自定义 Future

use std::pin::Pin;
use std::task::{Context, Poll};
use futures::Future;

struct MyFuture {
    value: i32,
}

impl Future for MyFuture {
    type Output = i32;
    
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Poll::Ready(self.value)
    }
}

小结

核心要点

  1. Future trait: 表示异步计算的 trait
  2. block_on: 在同步上下文执行 Future
  3. 链式组合: then, map, then 等方法
  4. 并发执行: join, select 组合多个 Future
  5. 错误处理: 使用 Result 和错误转换

关键术语

  • Future: 未来值/异步任务
  • Poll: 轮询
  • Executor: 执行器
  • Combinator: 组合子
  • block_on: 阻塞执行
  • await: 异步等待

术语表

English中文
Future未来值/异步任务
Poll轮询
Executor执行器
Combinator组合子
block_on阻塞执行
await异步等待
join并发等待
select选择第一个完成

知识检查

快速测验(答案在下方):

  1. Future 是惰性的还是立即执行的?

  2. poll() 返回 Pending 后,谁会再次调用 poll()

  3. async/await 和 Future 是什么关系?

点击查看答案与解析
  1. 惰性的 - 需要被 poll 才会执行
  2. Waker(由执行器提供)
  3. async/await 是 Future 的语法糖,编译器转换为状态机

关键理解: Future 本身不执行,需要执行器 (Executor) 驱动。

延伸阅读

学习完 Futures 后,你可能还想了解:

选择建议:

  • 想学习并行 → 继续学习 并行计算
  • 想学习底层 I/O → 跳到 MIO

继续学习

前一章: Tokio 异步运行时
下一章: 宏编程

相关章节:

返回: 高级进阶


完整示例: futures_sample.rs

Rayon 数据并行库

开篇故事

想象你是一家大型工厂的厂长。工厂里有一堆零件需要加工,传统做法是让一个工人从头到尾完成所有零件。这个工人干得再快,也只是一个 worker 在干活。

现在换一种思路:你把零件分成若干份,让每个工人处理一小批。工人们各司其职,互不干扰,最后汇总结果。这就是并行计算的威力。

但是管理多个工人也有挑战:

  • 如何分配任务?有的工人干得快,有的干得慢
  • 如何避免有的工人闲着,有的工人忙不过来?
  • 如何确保最终结果正确汇总?

Rayon 就是这个"智能工厂管理系统"——它是 Rust 生态中高性能的数据并行库,让你轻松地将顺序代码转换成并行代码,自动处理任务分配和负载均衡。最妙的是,它通过**工作窃取(Work Stealing)**算法,让空闲线程自动"偷取"忙碌线程的任务,确保所有 CPU 核心都被充分利用。


本章适合谁

如果你已经掌握了 Rust 基础,现在想要:

  • 利用多核 CPU 加速数据处理
  • 学习如何将顺序迭代器转换为并行迭代器
  • 理解工作窃取调度算法的原理
  • 掌握线程池和任务并行的高级用法

本章适合你。Rayon 的学习曲线非常平缓——很多时候,你只需要把 .iter() 改成 .par_iter(),就能立即获得并行加速。


你会学到什么

完成本章后,你可以:

  1. 解释什么是数据并行以及 Rayon 的核心优势
  2. 使用 par_iter()par_iter_mut() 进行并行迭代
  3. 使用 into_par_iter() 进行所有权转移的并行处理
  4. 使用 join() 并行执行两个独立任务
  5. 使用 scope() 创建嵌套并行任务
  6. 理解工作窃取调度的工作原理
  7. 避免常见的并行编程错误

前置要求

学习本章前,你需要理解:

  • 所有权 - 特别是移动语义和所有权转移
  • 闭包 - 闭包作为并行操作的参数
  • 迭代器 - 迭代器的基本用法
  • Cargo.toml 中添加 rayon 依赖

添加依赖

[dependencies]
rayon = "1.7"

第一个例子

让我们从一个最简单的并行计算开始——将数组中的每个元素乘以 2:

use rayon::prelude::*;

fn main() {
    let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // 顺序处理
    println!("顺序处理:");
    for x in &data {
        print!("{} ", x * 2);
    }
    println!();

    // 并行处理 - 只需添加 par_ 前缀!
    println!("并行处理:");
    data.par_iter().for_each(|x| {
        print!("{} ", x * 2);
    });
    println!();
}

发生了什么?

  1. use rayon::prelude::* 导入 Rayon 的 trait,使 .par_iter() 方法可用
  2. .par_iter() 创建一个并行迭代器
  3. .for_each() 并行处理每个元素
  4. 执行顺序不确定,但每个元素都会被处理

原理解析

数据并行 vs 任务并行

数据并行 (Data Parallelism)          任务并行 (Task Parallelism)
═══════════════════════════════════════════════════════════════════

同一份代码处理不同数据               不同代码并发执行

┌─────────┐                         ┌─────────┐  ┌─────────┐
│ 任务 A  │                         │ 任务 A  │  │ 任务 B  │
├─────────┤                         ├─────────┤  ├─────────┤
│ 数据 1  │──→ 线程1                 │ 下载图片 │  │ 分析日志 │
│ 数据 2  │──→ 线程2                 └─────────┘  └─────────┘
│ 数据 3  │──→ 线程3                         ↓           ↓
│ 数据 4  │──→ 线程4                    ┌─────────────────────┐
└─────────┘                            │     线程池调度      │
                                       └─────────────────────┘

Rayon 主要专注于数据并行,通过并行迭代器让同一份操作在多个数据上并发执行。

Rayon 的核心设计:工作窃取(Work Stealing)

工作窃取调度器的工作原理:

初始状态:                           工作窃取发生后:
┌─────────┐  ┌─────────┐            ┌─────────┐  ┌─────────┐
│ 线程 1  │  │ 线程 2  │            │ 线程 1  │  │ 线程 2  │
├─────────┤  ├─────────┤            ├─────────┤  ├─────────┤
│ 任务 1  │  │ 任务 3  │            │ 任务 1  │  │ 任务 3  │
│ 任务 2  │  │ 任务 4  │            │ 任务 2  │  │ 任务 4  │
│ 任务 5  │  │ 任务 6  │            │         │  │ 任务 6  │
│ 任务 7  │  │ 任务 8  │            │ 任务 8  │◄─┤ 任务 7  │  ← 线程2窃取任务7
│ 任务 9  │  │ 任务 10 │            │ 任务 9  │  │ 任务 10 │
│ 任务 11 │  │         │            │ 任务 11 │  │         │
│ 任务 12 │  │         │            │ 任务 12 │  │         │
└─────────┘  └─────────┘            └─────────┘  └─────────┘
  忙碌 ↑       空闲 ↓                  两个线程都在工作!

关键点:
1. 每个线程有自己的任务队列
2. 当线程空闲时,从其他线程"窃取"任务
3. 窃取从队列尾部开始,减少与所有者线程的冲突
4. 确保负载均衡,所有 CPU 核心都被利用

并行迭代器类型

Rayon 提供了三种主要的并行迭代器:

三种并行迭代方式:

┌─────────────────────────────────────────────────────────────────┐
│  原始数据                                                       │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]                      │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
           │
           ▼
┌────────────────────┬────────────────────┬──────────────────────┐
│   par_iter()       │ par_iter_mut()     │ into_par_iter()      │
├────────────────────┼────────────────────┼──────────────────────┤
│ 不可变借用           │ 可变借用             │ 转移所有权            │
│ &self               │ &mut self          │ self                 │
├────────────────────┼────────────────────┼──────────────────────┤
│ data.par_iter()    │ data.par_iter_mut()│ data.into_par_iter() │
├────────────────────┼────────────────────┼──────────────────────┤
│ 原数据仍可用         │ 原数据仍可用         │ 原数据不可用          │
│ 只读操作             │ 可修改元素           │ 消耗性操作            │
└────────────────────┴────────────────────┴──────────────────────┘

1. par_iter() - 不可变并行迭代

let numbers = vec![1, 2, 3, 4, 5];

// 并行求和
let sum: i32 = numbers.par_iter().sum();

// 并行过滤并收集
let evens: Vec<&i32> = numbers.par_iter().filter(|&&x| x % 2 == 0).collect();

2. par_iter_mut() - 可变并行迭代

let mut data = vec![1, 2, 3, 4, 5];

// 并行修改每个元素
data.par_iter_mut().for_each(|x| {
    *x *= 2;
});
// data 现在是 [2, 4, 6, 8, 10]

3. into_par_iter() - 所有权转移并行迭代

let words = vec!["hello".to_string(), "world".to_string()];

// 转移所有权,并行转换
let upper: Vec<String> = words
    .into_par_iter()
    .map(|s| s.to_uppercase())
    .collect();

// words 变量在这里不再可用

线程池管理

Rayon 自动管理全局线程池,默认线程数等于 CPU 核心数:

use rayon::ThreadPoolBuilder;

// 自定义线程池配置
let pool = ThreadPoolBuilder::new()
    .num_threads(4)           // 使用 4 个线程
    .thread_name(|i| format!("worker-{}", i))  // 自定义线程名
    .build()
    .unwrap();

// 在自定义线程池中执行
pool.install(|| {
    let sum: i32 = (0..100).into_par_iter().sum();
    println!("Sum: {}", sum);
});

常见错误

错误 1: 并行执行顺序不确定

// ❌ 错误:期望按顺序输出
let data = vec![1, 2, 3, 4, 5];
data.par_iter().for_each(|x| {
    print!("{} ", x);  // 输出顺序可能是 3 1 4 2 5
});

// ✅ 正确:如果需要顺序,使用 collect
let results: Vec<i32> = data.par_iter().map(|x| x * 2).collect();
// results 保持原始顺序

问题:并行迭代器不保证执行顺序,只保证每个元素被处理。


错误 2: 在并行代码中使用非线程安全类型

use std::cell::RefCell;

// ❌ 错误:RefCell 不是线程安全的
let counter = RefCell::new(0);
(0..100).into_par_iter().for_each(|_| {
    *counter.borrow_mut() += 1;  // 编译错误!
});

// ✅ 正确:使用原子类型或 Rayon 的 reduce
use std::sync::atomic::{AtomicUsize, Ordering};
let counter = AtomicUsize::new(0);
(0..100).into_par_iter().for_each(|_| {
    counter.fetch_add(1, Ordering::Relaxed);
});

// 或者更好的方式 - 使用并行归约
let sum: usize = (0..100).into_par_iter().sum();

错误 3: 过度并行化(小数据集)

// ❌ 不推荐:小数据集并行化开销大于收益
let small = vec![1, 2, 3];
let sum = small.par_iter().sum();  // 顺序执行可能更快

// ✅ 正确:大数据集才使用并行
let large: Vec<i32> = (0..1_000_000).collect();
let sum = large.par_iter().sum();  // 并行优势明显

指导原则:数据量小于 1000 时,顺序执行可能更快。Rayon 有内置启发式来决定是否真正并行执行。


动手练习

练习 1: 并行过滤与归约

补全下面的代码,实现并行计算偶数平方和:

use rayon::prelude::*;

fn sum_of_even_squares(numbers: &[i32]) -> i32 {
    // 你的代码:
    // 1. 并行迭代
    // 2. 过滤出偶数
    // 3. 映射为平方
    // 4. 求和
    
}

fn main() {
    let numbers: Vec<i32> = (1..=100).collect();
    let result = sum_of_even_squares(&numbers);
    println!("偶数平方和: {}", result);  // 期望输出: 171700
}
点击查看答案
fn sum_of_even_squares(numbers: &[i32]) -> i32 {
    numbers
        .par_iter()
        .filter(|&&x| x % 2 == 0)  // 过滤偶数
        .map(|x| x * x)            // 计算平方
        .sum()                     // 并行求和
}

解析

  • .par_iter() 开始并行迭代
  • 过滤和映射操作自动并行化
  • .sum() 使用高效的并行归约算法

练习 2: 使用 join 并行执行任务

使用 rayon::join 并行计算两个独立任务的结果:

use rayon::join;
use std::thread;
use std::time::Duration;

fn fetch_data_from_db() -> Vec<i32> {
    thread::sleep(Duration::from_secs(1));
    vec![1, 2, 3, 4, 5]
}

fn fetch_data_from_api() -> Vec<i32> {
    thread::sleep(Duration::from_secs(1));
    vec![6, 7, 8, 9, 10]
}

fn main() {
    // 你的代码:使用 join 并行执行两个 fetch 函数
    // 然后合并结果并计算总和
    
}
点击查看答案
fn main() {
    // join 并行执行两个任务
    let (db_data, api_data) = join(
        fetch_data_from_db,
        fetch_data_from_api
    );
    
    // 合并结果
    let all_data: Vec<i32> = db_data.into_iter()
        .chain(api_data.into_iter())
        .collect();
    
    let sum: i32 = all_data.par_iter().sum();
    println!("总和: {}", sum);  // 输出: 55
    
    // 总耗时约 1 秒,而不是 2 秒!
}

关键点

  • join 并行执行两个闭包
  • 等待两者都完成后返回结果
  • 适用于两个独立任务并行化

练习 3: 理解所有权转移

预测以下代码的输出,并解释为什么:

use rayon::prelude::*;

fn main() {
    let words = vec![
        "hello".to_string(),
        "world".to_string(),
        "rust".to_string(),
    ];
    
    // A. 使用 par_iter
    let upper1: Vec<String> = words.par_iter()
        .map(|s| s.to_uppercase())
        .collect();
    println!("{:?}", words);  // ❓ 可以编译吗?
    
    // B. 使用 into_par_iter
    let upper2: Vec<String> = words.into_par_iter()
        .map(|s| s.to_uppercase())
        .collect();
    println!("{:?}", words);  // ❓ 可以编译吗?
}
点击查看解析

答案

A 部分:✅ 可以编译,words 仍然可用

  • par_iter() 只借用数据,不转移所有权
  • map 中的闭包接收 &String,需要 to_uppercase() 创建新 String

B 部分:❌ 编译错误

  • into_par_iter() 转移所有权
  • words 被消耗,后续不能再使用

正确版本

fn main() {
    let words = vec![
        "hello".to_string(),
        "world".to_string(),
        "rust".to_string(),
    ];
    
    // A 部分 - 借用版本
    let upper1: Vec<String> = words.par_iter()
        .map(|s| s.to_uppercase())
        .collect();
    println!("A 之后 words 仍可用: {:?}", words);
    
    // B 部分 - 所有权转移版本
    let upper2: Vec<String> = words.into_par_iter()
        .map(|s| s.to_uppercase())
        .collect();
    // println!("{:?}", words);  // ❌ 编译错误:value used here after move
    println!("B 结果: {:?}", upper2);
}

故障排查 (FAQ)

Q: Rayon 和 Tokio 有什么区别?什么时候用哪个?

A:

特性RayonTokio
并行类型数据并行(CPU 密集型)异步 IO(IO 密集型)
主要用途大数据处理、计算网络服务、文件 IO
阻塞会阻塞线程非阻塞
典型场景图像处理、科学计算Web 服务器、数据库连接
// 使用 Rayon:CPU 密集型任务
let sum = (0..1_000_000).into_par_iter().map(|x| x * x).sum();

// 使用 Tokio:IO 密集型任务
let response = reqwest::get("https://api.example.com").await?;

Q: 如何控制 Rayon 的线程数?

A: 两种方式:

use rayon::ThreadPoolBuilder;

// 方式 1:设置全局线程池(在程序启动时调用)
ThreadPoolBuilder::new()
    .num_threads(4)
    .build_global()
    .unwrap();

// 方式 2:创建局部线程池
let pool = ThreadPoolBuilder::new()
    .num_threads(2)
    .build()
    .unwrap();

pool.install(|| {
    let sum: i32 = (0..100).into_par_iter().sum();
});

Q: Rayon 会保证使用所有线程吗?

A: 不一定。Rayon 使用自适应调度:

// 小数据集 - Rayon 可能选择顺序执行
let small_sum = [1, 2, 3].par_iter().sum();  // 可能单线程

// 大数据集 - Rayon 会使用多线程
let large_sum: i32 = (0..1_000_000).into_par_iter().sum();  // 多线程

Rayon 内部有阈值判断,小任务直接顺序执行反而更快(避免线程切换开销)。


Q: 如何在并行代码中处理错误?

A: 使用 try_for_eachtry_reduce

use rayon::prelude::*;

fn process_items(items: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    items.par_iter().try_for_each(|item| {
        if item.is_empty() {
            Err("Empty item found")?;
        }
        println!("Processing: {}", item);
        Ok(())
    })?;
    Ok(())
}

知识扩展 (选学)

自定义并行迭代器

为自定义类型实现并行迭代:

use rayon::prelude::*;
use rayon::iter::plumbing::*;

struct MyCollection<T>(Vec<T>);

impl<T: Send> IntoParallelIterator for MyCollection<T> {
    type Item = T;
    type Iter = rayon::vec::IntoIter<T>;
    
    fn into_par_iter(self) -> Self::Iter {
        self.0.into_par_iter()
    }
}

并行排序

Rayon 提供并行排序算法:

use rayon::prelude::*;

let mut data: Vec<i32> = vec![5, 2, 8, 1, 9, 3];
data.par_sort();  // 并行快速排序

并行递归

使用 join 实现分治算法:

fn parallel_sum(data: &[i32]) -> i32 {
    const THRESHOLD: usize = 1000;
    
    if data.len() <= THRESHOLD {
        data.iter().sum()
    } else {
        let mid = data.len() / 2;
        let (left, right) = data.split_at(mid);
        
        let (sum_left, sum_right) = rayon::join(
            || parallel_sum(left),
            || parallel_sum(right)
        );
        
        sum_left + sum_right
    }
}

小结

核心要点

  1. 数据并行是 Rayon 的核心——同一份操作在多个数据上并发执行
  2. 工作窃取算法自动负载均衡,空闲线程窃取忙碌线程的任务
  3. 三种迭代器par_iter()(借用)、par_iter_mut()(可变借用)、into_par_iter()(转移所有权)
  4. 低开销:只需添加 par_ 前缀,大部分顺序代码可直接并行化
  5. 自动调度:Rayon 自动决定是否真正并行,小任务可能顺序执行

关键术语

  • Data Parallelism(数据并行): 对大数据集并行执行相同操作
  • Work Stealing(工作窃取): 空闲线程从忙碌线程窃取任务的调度算法
  • Thread Pool(线程池): 预创建的线程集合,避免频繁创建销毁
  • Parallel Iterator(并行迭代器): 支持并行遍历的迭代器
  • Join: 并行执行两个任务并等待结果
  • Scope: 创建任务作用域,自动等待子任务完成

下一步

  • 学习 Tokio - 异步 IO 运行时
  • 理解 线程基础 - 并发编程基础
  • 探索 服务框架 - 生产级并行应用

术语表

English中文说明
Data Parallelism数据并行在多个数据上并行执行相同操作
Work Stealing工作窃取动态负载均衡调度算法
Thread Pool线程池复用线程的执行环境
Parallel Iterator并行迭代器支持并行遍历的迭代器 trait
Join任务合并并行执行两个任务并等待完成
Scope作用域任务的生命周期边界
Map映射对每个元素应用函数
Filter过滤按条件筛选元素
Reduce归约将多个值合并为单个值
Split分割将任务/数据分成多份

完整示例:src/advance/rayon_sample.rs


延伸阅读

学习完并行计算后,你可能还想了解:

选择建议:

  • 想学习底层 I/O → 继续学习 MIO
  • 想学习循环引用 → 跳到 循环引用

继续学习

💡 记住:Rayon 的设计哲学是"顺序代码优先,并行化简单"——先写正确的顺序代码,然后加上 par_ 前缀获得并行加速。让 Rayon 处理复杂的线程管理和负载均衡!


知识检查点

检查点 1 🟢 (基础概念)

以下代码的输出是什么?

use rayon::prelude::*;

fn main() {
    let data = vec![1, 2, 3, 4, 5];
    let sum: i32 = data.par_iter().sum();
    println!("{}", sum);
    println!("{:?}", data);
}

A) 编译错误,data 被消耗了
B) 输出 "15" 然后编译错误
C) 输出 "15" 然后 "[1, 2, 3, 4, 5]"
D) 运行时错误

答案与解析

答案: C) 输出 "15" 然后 "[1, 2, 3, 4, 5]"

解析:

  • par_iter() 只借用数据,不转移所有权
  • sum() 计算后会返回结果
  • data 仍然可用,可以正常打印

检查点 2 🟡 (所有权理解)

以下代码为什么不能编译?如何修复?

use rayon::prelude::*;

fn main() {
    let words = vec![
        "hello".to_string(),
        "world".to_string(),
    ];
    
    let upper: Vec<String> = words.into_par_iter()
        .map(|s| s.to_uppercase())
        .collect();
    
    println!("原始: {:?}", words);  // ❌ 编译错误
    println!("大写: {:?}", upper);
}

A) into_par_iter() 转移了所有权,words 不能再使用
B) to_uppercase() 返回的不是 String
C) collect() 消耗了 upper
D) 应该用 par_iter() 代替

答案与解析

答案: A 和 D 都是正确的分析

解析:

  • into_par_iter() 转移 words 的所有权
  • 迭代完成后 words 被消耗,不能再访问

修复方案(二选一):

方案 1:使用 par_iter()(如果不需转移所有权)

let upper: Vec<String> = words.par_iter()
    .map(|s| s.to_uppercase())
    .collect();
println!("原始: {:?}", words);  // ✅ 可以访问

方案 2:调整代码顺序(如果确实需要转移所有权)

let upper: Vec<String> = words.into_par_iter()
    .map(|s| s.to_uppercase())
    .collect();
// 不再访问 words
println!("大写: {:?}", upper);

检查点 3 🔴 (并行 vs 顺序)

以下哪种场景最适合使用 Rayon?

A) 处理单个 HTTP 请求,需要异步等待响应
B) 对 1000 万个浮点数进行复杂的数学计算
C) 读取用户输入并立即响应
D) 管理数据库连接池

答案与解析

答案: B) 对 1000 万个浮点数进行复杂的数学计算

解析:

选项推荐方案原因
ATokio/asyncIO 密集型,需要异步等待
BRayonCPU 密集型,大数据集并行计算
C顺序执行用户交互,需要及时响应
D连接池库资源管理,非计算任务

Rayon 最适合数据并行场景——大数据集上的 CPU 密集型计算。


扩展阅读

官方资源

相关项目

进阶主题

  • NUMA 感知调度:多路服务器的内存局部性优化
  • SIMD 并行:结合 packed_simd 进行向量化计算
  • GPU 计算:结合 rust-gpucust 进行异构并行

Mio 异步 I/O 库

开篇故事

想象你是一位邮局管理员,负责处理成千上万封信件。传统方式是依次处理每封信(阻塞 I/O):拿起一封信 → 读取 → 回复 → 放下 → 拿下一封。如果某封信需要等待回复,你就干等着,什么也做不了。

更聪明的做法是同时观察所有信箱,哪个有新信件就处理哪个(非阻塞 I/O)。这就是 Mio 的核心思想——它提供低级的异步 I/O 原语,是 Tokio 等高级运行时的基石。


本章适合谁

如果你想深入理解 Rust 异步 I/O 的底层实现原理,或者需要构建高性能的网络服务,本章适合你。Mio 是 Tokio、Hyper 等库的核心依赖。


你会学到什么

完成本章后,你可以:

  1. 理解 Mio 的设计理念:为什么它是"底层"的
  2. 使用 Poll、Token、Event 实现事件循环
  3. 构建非阻塞 TCP 服务器
  4. 理解 epoll/kqueue 等系统调用抽象
  5. 区分 Mio 与 Tokio 的使用场景

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add mio --features os-poll,net

第一个例子

最简单的 Mio TCP 服务器:

use mio::{Events, Interest, Poll, Token};
use mio::net::TcpListener;
use std::io::{self, Read};

const SERVER: Token = Token(0);

fn main() -> io::Result<()> {
    // 创建 Poll(事件轮询器)
    let mut poll = Poll::new()?;
    let mut events = Events::with_capacity(128);

    // 绑定并监听端口
    let addr = "127.0.0.1:9000".parse()?;
    let mut server = TcpListener::bind(addr)?;

    // 注册服务器到 Poll
    poll.registry().register(
        &mut server,
        SERVER,
        Interest::READABLE,
    )?;

    println!("服务器启动在 127.0.0.1:9000");

    // 事件循环
    loop {
        // 阻塞等待事件
        poll.poll(&mut events, None)?;

        for event in events.iter() {
            match event.token() {
                SERVER => {
                    // 接受新连接
                    let (mut stream, _) = server.accept()?;
                    println!("新连接!");
                }
                _ => unreachable!(),
            }
        }
    }
}

发生了什么?

  • Poll::new() - 创建事件轮询器
  • register() - 注册 I/O 源到 Poll
  • poll() - 阻塞等待事件
  • Events - 存储就绪事件的集合

原理解析

1. Mio 事件循环架构

┌─────────────────────────────────────────────────────────┐
│                    Mio 事件循环                          │
└─────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────┐
│  Poll (事件轮询器)                                       │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │  TcpListener│  │  TcpStream  │  │  UdpSocket  │     │
│  │  Token(0)   │  │  Token(1)   │  │  Token(2)   │     │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘     │
└─────────┼────────────────┼────────────────┼─────────────┘
          │                │                │
          ▼                ▼                ▼
┌─────────────────────────────────────────────────────────┐
│  系统调用 (epoll/kqueue/IOCP)                            │
│  - Linux:   epoll_wait()                                │
│  - macOS:   kqueue()                                    │
│  - Windows: IOCP                                        │
└─────────────────────────────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────┐
│  Events (就绪事件集合)                                   │
│  ┌─────────────────────────────────────────────────┐   │
│  │ Event { token: Token(0), readable: true }       │   │
│  │ Event { token: Token(1), writable: true }       │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────┐
│  Handler (事件处理器)                                    │
│  - 根据 Token 分发事件                                   │
│  - 处理可读/可写事件                                     │
└─────────────────────────────────────────────────────────┘

2. 核心概念详解

Poll (事件轮询器):

let mut poll = Poll::new()?;
  • 封装操作系统的 I/O 多路复用机制
  • Linux 下使用 epoll
  • macOS 下使用 kqueue
  • Windows 下使用 IOCP

Token (标识符):

const SERVER: Token = Token(0);
const CLIENT: Token = Token(1);
  • 用户自定义的整数标识
  • 用于区分不同的 I/O 源
  • 必须唯一

Event (事件):

for event in events.iter() {
    if event.is_readable() {
        // 处理可读事件
    }
    if event.is_writable() {
        // 处理可写事件
    }
}

Interest (关注事件):

Interest::READABLE   // 关注可读事件
Interest::WRITABLE   // 关注可写事件
Interest::READABLE | Interest::WRITABLE  // 同时关注

3. 完整 TCP Echo 服务器

use mio::{Events, Interest, Poll, Token};
use mio::net::{TcpListener, TcpStream};
use std::collections::HashMap;
use std::io::{self, Read, Write};

const SERVER: Token = Token(0);

fn main() -> io::Result<()> {
    let mut poll = Poll::new()?;
    let mut events = Events::with_capacity(128);

    let addr = "127.0.0.1:9000".parse()?;
    let mut server = TcpListener::bind(addr)?;

    poll.registry().register(
        &mut server,
        SERVER,
        Interest::READABLE,
    )?;

    // 存储客户端连接
    let mut clients: HashMap<Token, TcpStream> = HashMap::new();
    let mut next_token = 1;

    println!("Echo 服务器启动在 127.0.0.1:9000");

    loop {
        poll.poll(&mut events, None)?;

        for event in events.iter() {
            match event.token() {
                SERVER => {
                    // 接受新连接
                    loop {
                        match server.accept() {
                            Ok((mut stream, _)) => {
                                let token = Token(next_token);
                                next_token += 1;

                                poll.registry().register(
                                    &mut stream,
                                    token,
                                    Interest::READABLE,
                                )?;

                                clients.insert(token, stream);
                                println!("新客户端连接:{}", token.0);
                            }
                            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
                                break;
                            }
                            Err(e) => {
                                eprintln!("接受连接失败:{}", e);
                                break;
                            }
                        }
                    }
                }
                token => {
                    // 处理客户端数据
                    if let Some(mut stream) = clients.remove(&token) {
                        let mut buf = [0; 1024];
                        match stream.read(&mut buf) {
                            Ok(0) => {
                                // 连接关闭
                                println!("客户端断开:{}", token.0);
                                poll.registry().deregister(&mut stream)?;
                            }
                            Ok(n) => {
                                // Echo 回数据
                                let data = &buf[..n];
                                stream.write_all(data)?;
                                println!("Echo {} 字节给客户端 {}", n, token.0);

                                // 重新注册
                                poll.registry().register(
                                    &mut stream,
                                    token,
                                    Interest::READABLE,
                                )?;
                                clients.insert(token, stream);
                            }
                            Err(e) => {
                                eprintln!("读取失败:{}", e);
                            }
                        }
                    }
                }
            }
        }
    }
}

4. Mio vs Tokio 对比

特性MioTokio
抽象级别低级(接近系统调用)高级(异步运行时)
编程模型手动事件循环async/await
任务调度工作窃取调度器
适用场景自定义运行时、极致性能大多数异步应用
学习曲线陡峭平缓
代码复杂度高(手动管理状态)低(编译器管理状态)

何时使用 Mio?

  • 构建自定义异步运行时
  • 需要极致性能控制
  • 学习异步 I/O 底层原理

何时使用 Tokio?

  • 构建普通网络服务
  • 需要丰富的生态系统
  • 快速开发

常见错误

错误 1: 忘记注册事件源

// ❌ 错误:未注册就等待事件
let mut stream = TcpStream::connect(addr)?;
poll.poll(&mut events, None)?;  // 永远不会收到事件

// ✅ 正确:先注册
let mut stream = TcpStream::connect(addr)?;
poll.registry().register(
    &mut stream,
    Token(1),
    Interest::READABLE,
)?;

错误 2: Token 冲突

// ❌ 错误:两个源使用相同 Token
poll.registry().register(&mut server, Token(0), Interest::READABLE)?;
poll.registry().register(&mut client, Token(0), Interest::READABLE)?;  // 冲突!

// ✅ 正确:使用唯一 Token
poll.registry().register(&mut server, Token(0), Interest::READABLE)?;
poll.registry().register(&mut client, Token(1), Interest::READABLE)?;

错误 3: 阻塞操作

// ❌ 错误:在事件循环中阻塞
loop {
    poll.poll(&mut events, None)?;
    for event in events.iter() {
        std::thread::sleep(Duration::from_secs(1));  // 阻塞整个事件循环!
    }
}

// ✅ 正确:非阻塞操作
loop {
    poll.poll(&mut events, None)?;
    for event in events.iter() {
        // 快速处理事件
        handle_event(event);
    }
}

动手练习

练习 1: 实现简单的 TCP 服务器

创建一个监听 9000 端口的服务器,接受连接并打印客户端地址:

// TODO: 实现服务器
// 1. 创建 Poll
// 2. 绑定 TcpListener
// 3. 注册到 Poll
// 4. 事件循环接受连接
点击查看答案
use mio::{Events, Interest, Poll, Token};
use mio::net::TcpListener;
use std::io;

const SERVER: Token = Token(0);

fn main() -> io::Result<()> {
    let mut poll = Poll::new()?;
    let mut events = Events::with_capacity(128);

    let addr = "127.0.0.1:9000".parse()?;
    let mut server = TcpListener::bind(addr)?;

    poll.registry().register(
        &mut server,
        SERVER,
        Interest::READABLE,
    )?;

    println!("服务器启动");

    loop {
        poll.poll(&mut events, None)?;
        for event in events.iter() {
            if event.token() == SERVER {
                if let Ok((stream, addr)) = server.accept() {
                    println!("新连接:{}", addr);
                }
            }
        }
    }
}

故障排查

Q: Mio 和 epoll 是什么关系?

A: Mio 是跨平台抽象层:

  • Linux → epoll
  • macOS → kqueue
  • Windows → IOCP

Q: 为什么 Mio 代码这么复杂?

A: Mio 是低级库,需要手动管理状态。大多数情况下应该使用 Tokio。

Q: 如何处理大量连接?

A: 使用 HashMap<Token, TcpStream> 存储连接,用递增 Token 标识。


小结

核心要点

  1. Poll: 事件轮询器,封装系统调用
  2. Token: 唯一标识 I/O 源
  3. Event: 就绪事件
  4. Interest: 关注的事件类型
  5. 事件循环: 持续轮询和处理事件

关键术语

  • Event Loop: 事件循环
  • Non-blocking I/O: 非阻塞 I/O
  • epoll/kqueue/IOCP: 系统调用
  • Token: 标识符
  • Registry: 注册表

术语表

English中文
Event Loop事件循环
Poll轮询器
Token标识符
Non-blocking I/O非阻塞 I/O
epollLinux 事件
kqueuemacOS 事件

完整示例:src/advance/mio_sample.rs


知识检查

快速测验(答案在下方):

  1. Mio 和 Tokio 是什么关系?

  2. Interest::READABLE 表示什么?

  3. 为什么事件循环中不能有阻塞操作?

点击查看答案与解析
  1. Mio 是底层 I/O 抽象层,Tokio 在其上构建运行时
  2. 关注源的可读事件(有数据到达)
  3. 阻塞操作会阻止事件循环处理其他就绪事件

关键理解: Mio 提供原始 I/O 原语,Tokio 提供高级异步抽象。

继续学习

💡 记住:Mio 是异步 I/O 的基石。理解它,你就能理解所有异步运行时!

循环引用

开篇故事

想象你和朋友互相保管对方的钥匙:你把家门钥匙给他,他把家门钥匙给你。现在你们都被锁在外面了——因为要拿钥匙需要对方开门,但对方也需要你的钥匙才能开门。这就是循环引用的本质:两个对象互相持有对方的引用,永远无法释放

在 Rust 中,RcArc 是引用计数的智能指针,但单纯的引用计数无法检测循环引用。本章就是为了解决这个问题而设计的。


本章适合谁

如果你需要使用 RcArc 构建复杂的数据结构(如图、树),担心循环引用导致内存泄漏,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 理解循环引用的成因和危害
  2. 使用 Weak 打破循环引用
  3. 识别何时会发生循环引用
  4. 设计避免循环的数据结构

前置要求

学习本章前,你需要理解:

  • 所有权 - 理解所有权和借用
  • 智能指针 - 理解 RcArc
  • 引用计数 - 理解引用计数原理

依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full

第一个例子

让我们看一个循环引用的例子:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    // 使用 Weak 打破循环
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

发生了什么?

  • children 使用 Rc<Node>(强引用)
  • parent 使用 Weak<Node>(弱引用)
  • 弱引用不增加引用计数,打破循环

原理解析

1. 循环引用的成因

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    let a = Rc::new(Node { value: 1, next: RefCell::new(None) });
    let b = Rc::new(Node { value: 2, next: RefCell::new(None) });

    // ❌ 创建循环引用
    *a.next.borrow_mut() = Some(Rc::clone(&b));
    *b.next.borrow_mut() = Some(Rc::clone(&a));

    // 即使 a 和 b 离开作用域,内存也不会释放
    // 因为引用计数永远 > 0
}

问题

  • a 持有 b 的强引用(计数 +1)
  • b 持有 a 的强引用(计数 +1)
  • 即使外部引用消失,内部引用仍在

2. 使用 Weak 打破循环

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    next: RefCell<Option<Rc<Node>>>,
    prev: RefCell<Option<Weak<Node>>>,  // 弱引用
}

fn main() {
    let a = Rc::new(Node { 
        value: 1, 
        next: RefCell::new(None),
        prev: RefCell::new(None),
    });
    
    let b = Rc::new(Node { 
        value: 2, 
        next: RefCell::new(None),
        prev: RefCell::new(None),
    });

    // ✅ 正确:一个强引用,一个弱引用
    *a.next.borrow_mut() = Some(Rc::clone(&b));
    *b.prev.borrow_mut() = Some(Rc::downgrade(&a));

    // 当 a 和 b 离开作用域,内存会被释放
}

解决方案

  • 单向关系使用 Rc(强引用)
  • 反向关系使用 Weak(弱引用)

3. Weak 的使用模式

use std::rc::{Rc, Weak};

fn main() {
    let strong = Rc::new(5);
    let weak = Rc::downgrade(&strong);

    // 检查弱引用是否有效
    if let Some(value) = weak.upgrade() {
        println!("值:{}", value);
    } else {
        println!("弱引用已失效");
    }

    // 获取引用计数
    println!("强引用计数:{}", Rc::strong_count(&strong));
    println!("弱引用计数:{}", Rc::weak_count(&strong));

    drop(strong);

    // 强引用释放后,弱引用失效
    assert!(weak.upgrade().is_none());
}

关键点

  • Weak::upgrade() 返回 Option<Rc<T>>
  • 如果强引用还在,返回 Some(rc)
  • 如果强引用已释放,返回 None

4. 典型应用场景

场景 1: 树结构

struct TreeNode {
    value: i32,
    children: Vec<Rc<TreeNode>>,  // 子节点:强引用
    parent: RefCell<Weak<TreeNode>>,  // 父节点:弱引用
}

场景 2: 图结构

struct GraphNode {
    value: i32,
    neighbors: Vec<Rc<GraphNode>>,  // 出边:强引用
    incoming: RefCell<Vec<Weak<GraphNode>>>,  // 入边:弱引用
}

场景 3: 观察者模式

struct Subject {
    observers: RefCell<Vec<Weak<Observer>>>,  // 观察者:弱引用
}

impl Subject {
    fn notify(&self) {
        // 清理失效的弱引用
        self.observers.borrow_mut().retain(|weak| {
            if let Some(observer) = weak.upgrade() {
                observer.update();
                true
            } else {
                false  // 自动清理
            }
        });
    }
}

常见错误

错误 1: 双向都使用强引用

// ❌ 错误:内存泄漏
struct Node {
    next: Option<Rc<Node>>,
    prev: Option<Rc<Node>>,  // 应该是 Weak<Node>
}

修复:反向引用使用 Weak

错误 2: 忘记检查 Weak 有效性

// ❌ 错误:直接 unwrap
let parent = self.parent.borrow().upgrade().unwrap();

// ✅ 正确:检查有效性
if let Some(parent) = self.parent.borrow().upgrade() {
    // 使用 parent
} else {
    // 处理父节点不存在的情况
}

错误 3: 滥用 Weak 导致频繁失败

// ❌ 错误:过度使用 Weak
struct Cache {
    data: Weak<Data>,  // 可能总是失效
}

// ✅ 正确:缓存应该持有强引用
struct Cache {
    data: Rc<Data>,
}

动手练习

练习 1: 创建双向链表

使用 RcWeak 创建双向链表:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    next: RefCell<Option<Rc<Node>>>,
    prev: RefCell<Option<Weak<Node>>>,
}

// TODO: 实现创建和遍历函数
点击查看答案
impl Node {
    fn new(value: i32) -> Rc<Node> {
        Rc::new(Node {
            value,
            next: RefCell::new(None),
            prev: RefCell::new(None),
        })
    }

    fn append(this: &Rc<Node>, next: Rc<Node>) {
        *next.prev.borrow_mut() = Some(Rc::downgrade(this));
        *this.next.borrow_mut() = Some(next);
    }
}

解析: next 使用强引用,prev 使用弱引用,避免循环。


故障排查

Q: 什么时候使用 Weak?

A: 当你需要"观察"或"回溯"但不拥有对象时:

  • 父节点引用(子节点不拥有父节点)
  • 观察者模式(观察者不拥有被观察者)
  • 缓存反向索引

Q: Weak 的性能开销?

A:

  • upgrade() 是原子操作(轻微开销)
  • 弱引用计数需要维护
  • 但对于打破循环的价值,开销可接受

Q: 如何调试循环引用?

A:

println!("强引用计数:{}", Rc::strong_count(&rc));
println!("弱引用计数:{}", Rc::weak_count(&rc));

如果预期应该释放但计数 > 0,可能存在循环。


小结

核心要点

  1. 循环引用: 两个对象互相持有强引用,无法释放
  2. Weak 智能指针: 弱引用不增加计数,打破循环
  3. upgrade(): 安全地从弱引用获取强引用
  4. 设计模式: 单向强引用,反向弱引用

关键术语

  • Strong Reference: 强引用,增加引用计数
  • Weak Reference: 弱引用,不增加计数
  • Cycle Detection: 循环检测
  • Memory Leak: 内存泄漏

下一步

  • 学习 智能指针
  • 理解 Rc 和 Arc
  • 探索 观察者模式

术语表

English中文
Reference Count引用计数
Cycle循环
Weak Reference弱引用
Strong Reference强引用
Memory Leak内存泄漏
Upgrade升级

完整示例:src/advance/async/cyclerc_sample.rs


知识检查

快速测验(答案在下方):

  1. 为什么 Rc 会导致循环引用问题?

  2. Weak::upgrade() 返回什么类型?

  3. 如何检测循环引用?

点击查看答案与解析
  1. Rc 是强引用,互相持有导致引用计数永远 > 0
  2. Option<Rc<T>> - 如果强引用还在返回 Some,否则 None
  3. 使用 Weak 弱引用打破循环,或使用调试工具检查引用计数

关键理解: 循环引用是 Rust 中少数会导致内存泄漏的情况。

延伸阅读

学习完循环引用检测后,你可能还想了解:

选择建议:

继续学习

  • 下一步:智能指针
  • 进阶:无锁编程
  • 回顾:所有权

💡 记住:循环引用是 Rust 中少数会导致内存泄漏的情况。使用 Weak 打破循环,确保内存安全!

数据库操作

开篇故事

想象你去餐厅吃饭。传统方式是:看菜单 → 告诉服务员 → 等待上菜。数据库编程就像这个过程:应用程序发出 SQL 查询 → 数据库执行 → 返回结果。Rust 的数据库库(SQLx 和 Diesel)就像智能点餐系统——在下单前就告诉你菜品是否存在、是否符合饮食限制。

在 Rust 中,数据库操作分为两大流派:SQLx(异步、编译时检查)和 Diesel(ORM、类型安全)。本章介绍这两种方法的核心概念。


本章适合谁

如果你需要在 Rust 程序中存储和检索数据,本章适合你。无论你是构建 Web 应用、CLI 工具还是微服务,数据库都是不可或缺的部分。


你会学到什么

完成本章后,你可以:

  1. 选择适合的数据库库(SQLx vs Diesel)
  2. 理解异步数据库操作的优势
  3. 掌握类型安全查询的原理
  4. 设计数据库连接管理策略

前置要求

学习本章前,你需要理解:


依赖安装

运行以下命令安装所需依赖:

cargo add sqlx --features runtime-tokio,postgres
cargo add diesel --features postgres

第一个例子

让我们看一个最简单的 SQLx 查询示例:

use sqlx::{Pool, Postgres, Row};

async fn get_users(pool: &Pool<Postgres>) -> sqlx::Result<Vec<String>> {
    let rows = sqlx::query("SELECT name FROM users")
        .fetch_all(pool)
        .await?;
    
    Ok(rows.iter().map(|row| row.get(0)).collect())
}

发生了什么?

  • Pool<Postgres>: PostgreSQL 连接池
  • query(): 编译时检查 SQL 语法
  • fetch_all(): 异步执行查询
  • Row::get(): 类型安全的列访问

原理解析

1. SQLx vs Diesel 对比

特性SQLxDiesel
查询方式原生 SQLDSL(领域特定语言)
检查时机编译时编译时
异步支持✅ 原生异步❌ 同步(需手动包装)
ORM 功能有限完整
学习曲线低(会 SQL 即可)中(需学 DSL)
适合场景复杂查询、存储过程CRUD、业务逻辑

2. SQLx 编译时检查

// 编译时会检查 SQL 语法和表结构
let user = sqlx::query_as!(
    User,
    r#"SELECT id, name, email FROM users WHERE id = $1"#,
    user_id
)
.fetch_one(&pool)
.await?;

优势

  • SQL 语法错误在编译时发现
  • 表结构变更立即报错
  • 参数类型自动推断

3. Diesel ORM 模式

// schema.rs - 数据库模式
table! {
    users (id) {
        id -> Integer,
        name -> Text,
        email -> Text,
    }
}

// models.rs - 数据模型
#[derive(Queryable)]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,
}

// 查询
let users = users::table
    .filter(users::email.eq("test@example.com"))
    .load::<User>(&conn)?;

优势

  • 完全类型安全
  • Rust 代码即查询
  • 自动关联映射

4. 连接池管理

use sqlx::postgres::PgPoolOptions;

async fn create_pool() -> sqlx::Result<Pool<Postgres>> {
    PgPoolOptions::new()
        .max_connections(5)
        .min_connections(2)
        .acquire_timeout(std::time::Duration::from_secs(30))
        .connect("postgresql://localhost/mydb")
        .await
}

配置要点

  • max_connections: 最大连接数
  • min_connections: 最小空闲连接
  • acquire_timeout: 获取连接超时

常见错误

错误 1: 连接池过小

// ❌ 错误:连接池太小
PgPoolOptions::new().max_connections(1)

// ✅ 正确:根据负载配置
PgPoolOptions::new().max_connections(10)

错误 2: 忘记处理 NULL

// ❌ 错误:假设列非空
let name: String = row.get(0);

// ✅ 正确:处理 NULL
let name: Option<String> = row.get(0);

错误 3: 在事务中长时间持有连接

// ❌ 错误:事务未提交
let mut tx = pool.begin().await?;
// ... 长时间操作 ...
// 忘记 tx.commit().await

// ✅ 正确:及时提交或回滚
let mut tx = pool.begin().await?;
// ... 操作 ...
tx.commit().await?;

动手练习

练习 1: 创建用户表

使用 SQLx 创建用户表并插入数据:

// TODO: 实现创建表和插入用户
async fn create_user(pool: &Pool<Postgres>, name: &str, email: &str) -> sqlx::Result<i32> {
    // 实现代码
}
点击查看答案
async fn create_user(pool: &Pool<Postgres>, name: &str, email: &str) -> sqlx::Result<i32> {
    let row = sqlx::query_scalar!(
        r#"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id"#,
        name,
        email
    )
    .fetch_one(pool)
    .await?;
    
    Ok(row)
}

解析: 使用 query_scalar! 返回单个值。


故障排查

Q: SQLx 编译时检查失败怎么办?

A: 确保:

  1. 数据库正在运行
  2. 连接字符串正确
  3. 表结构已创建

Q: Diesel 迁移如何管理?

A: 使用 Diesel CLI:

diesel migration generate create_users
diesel migration run

Q: 连接池耗尽如何处理?

A:

  • 增加 max_connections
  • 检查是否有未释放的连接
  • 使用连接池监控

小结

核心要点

  1. SQLx: 异步、编译时检查、原生 SQL
  2. Diesel: ORM、类型安全、DSL 查询
  3. 连接池: 管理数据库连接复用
  4. 错误处理: 使用 Result 传播数据库错误

关键术语

  • Connection Pool: 连接池
  • ORM: 对象关系映射
  • Query Builder: 查询构建器
  • Migration: 数据库迁移

下一步


术语表

English中文
Connection Pool连接池
ORM对象关系映射
Query查询
Transaction事务
Migration迁移
Prepared Statement预编译语句

完整示例:src/advance/database/sqlx_sample.rs, src/advance/database/diesel_sample.rs


知识检查

快速测验(答案在下方):

  1. SQLx 和 Diesel 的核心区别是什么?

  2. 连接池的作用是什么?

  3. 编译时 SQL 检查是如何工作的?

点击查看答案与解析
  1. SQLx = 异步 + 原生 SQL,Diesel = 同步 + ORM
  2. 复用数据库连接,避免频繁创建/销毁
  3. 连接数据库验证 SQL 语法和表结构

关键理解: 选择数据库库取决于你的需求:异步用 SQLx,ORM 用 Diesel。

继续学习

💡 记住:选择合适的数据库库取决于你的需求。复杂查询用 SQLx,业务逻辑用 Diesel!

Diesel ORM

开篇故事

想象你要写 SQL 查询。传统方式是:手写 SQL → 执行 → 手动映射结果 → 处理类型错误。Diesel 就像智能助手——你定义数据结构,它生成类型安全的 SQL,编译时就告诉你有没有错误。


本章适合谁

如果你需要在 Rust 程序中使用 ORM(对象关系映射)操作数据库,本章适合你。Diesel 是类型安全的 ORM,在编译时捕获 SQL 错误。


你会学到什么

完成本章后,你可以:

  1. 定义 Diesel Schema
  2. 创建数据模型结构体
  3. 执行 CRUD 操作
  4. 使用类型安全的查询构建器
  5. 处理数据库连接和事务

前置要求

  • 结构体 - 结构体定义
  • Serde 序列化 - 序列化基础
  • SQLx 数据库 - 数据库基础(可选)

依赖安装

运行以下命令安装所需依赖:

cargo add diesel --features postgres
cargo add dotenvy

第一个例子

最简单的 Diesel SQLite 示例:

use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;

// 定义 Schema
diesel::table! {
    posts (id) {
        id -> Integer,
        title -> Text,
        body -> Text,
        published -> Bool,
    }
}

// 定义模型
#[derive(Queryable)]
struct Post {
    id: i32,
    title: String,
    body: String,
    published: bool,
}

fn main() {
    // 连接数据库
    let mut conn = SqliteConnection::establish("test.db")
        .expect("Error connecting to database");
    
    // 查询数据
    let posts = posts::table
        .load::<Post>(&mut conn)
        .expect("Error loading posts");
    
    for post in posts {
        println!("{}: {}", post.title, post.body);
    }
}

完整示例: diesel_sample.rs


原理解析

Diesel 特性

Diesel 是一个 ORM

  • ✅ 编译时 SQL 验证
  • ✅ 类型安全查询
  • ✅ 自动映射结果
  • ✅ 支持多种数据库(SQLite, MySQL, PostgreSQL)

Schema 定义

使用 diesel::table! 宏

diesel::table! {
    users (id) {
        id -> Integer,
        name -> Text,
        email -> Text,
        age -> Integer,
    }
}

字段类型映射

  • Integer → i32
  • Text → String
  • Bool → bool
  • Nullable<Text> → Option

模型定义

使用 Queryable derive

use diesel::Queryable;

#[derive(Queryable)]
struct User {
    id: i32,
    name: String,
    email: String,
    age: i32,
}

使用 Insertable derive

use diesel::Insertable;

#[derive(Insertable)]
#[diesel(table_name = users)]
struct NewUser<'a> {
    name: &'a str,
    email: &'a str,
    age: i32,
}

连接数据库

建立连接

use diesel::sqlite::SqliteConnection;
use diesel::Connection;

let mut conn = SqliteConnection::establish("database.db")
    .expect("Error connecting to database");

使用环境变量

use dotenvy::dotenv;
use std::env;

dotenv().ok();
let database_url = env::var("DATABASE_URL")
    .unwrap_or_else(|_| "test.db".into());

let mut conn = SqliteConnection::establish(&database_url)
    .expect("Error connecting to database");

CRUD 操作

Create (插入)

use diesel::RunQueryDsl;

let new_post = NewPost {
    title: "My Post",
    body: "Post body",
    published: true,
};

diesel::insert_into(posts::table)
    .values(&new_post)
    .execute(&mut conn)
    .expect("Error saving new post");

Read (查询)

use diesel::RunQueryDsl;

// 查询所有
let all_posts = posts::table
    .load::<Post>(&mut conn)
    .expect("Error loading posts");

// 查询单个
let post = posts::table
    .find(post_id)
    .first::<Post>(&mut conn)
    .expect("Error loading post");

// 条件查询
let published_posts = posts::table
    .filter(posts::published.eq(true))
    .load::<Post>(&mut conn)
    .expect("Error loading posts");

Update (更新)

use diesel::RunQueryDsl;

diesel::update(posts::table.find(post_id))
    .set(posts::published.eq(true))
    .execute(&mut conn)
    .expect("Error updating post");

Delete (删除)

diesel::delete(posts::table.find(post_id))
    .execute(&mut conn)
    .expect("Error deleting post");

查询构建器

过滤

posts::table
    .filter(posts::published.eq(true))
    .filter(posts::title.like("%Rust%"))
    .load::<Post>(&mut conn)?;

排序

posts::table
    .order(posts::created_at.desc())
    .load::<Post>(&mut conn)?;

限制数量

posts::table
    .limit(10)
    .offset(20)
    .load::<Post>(&mut conn)?;

连接表

posts::table
    .inner_join(users::table)
    .select((posts::all_columns, users::name))
    .load::<(Post, String)>(&mut conn)?;

常见错误

错误 1: Schema 不匹配

// Schema 定义
diesel::table! {
    posts (id) {
        id -> Integer,
        title -> Text,
    }
}

// 模型
#[derive(Queryable)]
struct Post {
    id: i32,
    title: String,
    body: String,  // ❌ 数据库中不存在 body 字段
}

错误信息:

column `body` does not exist

修复方法:

#[derive(Queryable)]
struct Post {
    id: i32,
    title: String,  // ✅ 只包含存在的字段
}

错误 2: 忘记 derive

// ❌ 忘记 #[derive(Queryable)]
struct Post {
    id: i32,
    title: String,
}

错误信息:

the trait `Queryable<_, __>` is not implemented for `Post`

修复方法:

#[derive(Queryable)]  // ✅ 添加 derive
struct Post {
    id: i32,
    title: String,
}

错误 3: 连接未使用 mut

let conn = SqliteConnection::establish("test.db")?;
posts::table.load::<Post>(&conn)?;  // ❌ 需要 &mut conn

修复方法:

let mut conn = SqliteConnection::establish("test.db")?;
posts::table.load::<Post>(&mut conn)?;  // ✅ 使用 &mut

动手练习

练习 1: 定义用户 Schema

// TODO: 定义 users 表 Schema
// 字段:id (Integer), name (Text), email (Text)

// TODO: 定义 User 结构体
// 实现 Queryable trait
点击查看答案
diesel::table! {
    users (id) {
        id -> Integer,
        name -> Text,
        email -> Text,
    }
}

#[derive(Queryable)]
struct User {
    id: i32,
    name: String,
    email: String,
}

练习 2: 插入和查询用户

// TODO: 创建数据库连接
// TODO: 插入 3 个用户
// TODO: 查询所有用户
// TODO: 打印用户列表
点击查看答案
let mut conn = SqliteConnection::establish("test.db")?;

// 插入
let new_user = NewUser {
    name: "Alice",
    email: "alice@example.com",
};

diesel::insert_into(users::table)
    .values(&new_user)
    .execute(&mut conn)?;

// 查询
let users = users::table.load::<User>(&mut conn)?;

for user in users {
    println!("{} - {}", user.name, user.email);
}

练习 3: 条件查询

// TODO: 查询已发布的博客
// TODO: 按创建时间排序
// TODO: 限制返回 10 条
点击查看答案
let posts = posts::table
    .filter(posts::published.eq(true))
    .order(posts::created_at.desc())
    .limit(10)
    .load::<Post>(&mut conn)?;

故障排查 (FAQ)

Q: Diesel 和 SQLx 有什么区别?

A:

  • Diesel: ORM,编译时 SQL 验证,类型安全更强
  • SQLx: 异步,运行时 SQL 验证,更灵活
  • 推荐: Diesel(类型安全更好)

Q: 如何处理关联关系?

A:

#[derive(Associations, Queryable)]
#[diesel(belongs_to(User))]
struct Post {
    id: i32,
    user_id: i32,
    title: String,
}

Q: 如何运行迁移?

A:

# 安装 diesel CLI
cargo install diesel_cli

# 创建迁移
diesel migration generate create_posts_table

# 运行迁移
diesel migration run

知识扩展

事务处理

use diesel::Connection;

conn.transaction::<_, diesel::result::Error, _>(|conn| {
    // 所有操作在一个事务中
    diesel::insert_into(posts::table)
        .values(&new_post)
        .execute(conn)?;
    
    diesel::insert_into(comments::table)
        .values(&new_comment)
        .execute(conn)?;
    
    Ok(())
})?;

关联查询

// 查询用户及其所有博客
let results = users::table
    .inner_join(posts::table)
    .select((User::as_select(), Post::as_select()))
    .load::<(User, Post)>(&mut conn)?;

动态查询

let mut query = posts::table.into_boxed();

if let Some(title) = filter_title {
    query = query.filter(posts::title.like(format!("%{}%", title)));
}

if let Some(user) = filter_user {
    query = query.filter(posts::user_id.eq(user.id));
}

let posts = query.load::<Post>(&mut conn)?;

小结

核心要点

  1. Diesel: 类型安全的 ORM
  2. Schema: 使用 table! 宏定义
  3. Queryable: 自动映射查询结果
  4. Insertable: 类型安全的插入
  5. 查询构建器: filter, order, limit 等
  6. 事务: 使用 transaction 方法

关键术语

  • ORM (对象关系映射): 数据库表映射到结构体
  • Schema: 数据库结构定义
  • Queryable: 查询结果映射 trait
  • Insertable: 插入数据 trait
  • Transaction: 数据库事务

术语表

English中文
ORM对象关系映射
Schema模式/架构
Queryable可查询
Insertable可插入
Transaction事务
Migration迁移

知识检查

快速测验(答案在下方):

  1. Diesel 的 schema 是什么?

  2. QueryableInsertable 有什么区别?

  3. 如何处理数据库迁移?

点击查看答案与解析
  1. 数据库表结构的 Rust 表示(table! 宏)
  2. Queryable = 查询结果映射,Insertable = 插入数据映射
  3. 使用 Diesel CLI:diesel migration generatediesel migration run

关键理解: Diesel 是类型安全的 ORM,编译时检查查询。

继续学习

前一章: Futures 异步编程
下一章: 宏编程

相关章节:

返回: 高级进阶


完整示例: diesel_sample.rs

SQLx 数据库操作

开篇故事

想象你去银行办业务。传统方式是:排队 → 到窗口 → 说明需求 → 等待办理 → 完成。SQLx 就像银行的自助终端——你提交请求,它异步处理,完成后通知你。这样你不需要一直等待,可以同时处理其他事情。


本章适合谁

如果你需要在 Rust 程序中操作数据库(SQLite、MySQL、PostgreSQL 等),本章适合你。SQLx 是类型安全的异步数据库库,是构建数据驱动应用的首选。


你会学到什么

完成本章后,你可以:

  1. 使用 SQLx 连接数据库
  2. 执行 SQL 查询和插入操作
  3. 使用类型安全的查询绑定
  4. 将查询结果映射到结构体
  5. 使用连接池管理数据库连接

前置要求

  • Tokio 异步运行时 - 异步基础
  • 结构体 - 结构体定义
  • Serde 序列化 - 序列化基础

依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add sqlx --features runtime-tokio,postgres

第一个例子

最简单的 SQLx SQLite 示例:

use sqlx::sqlite::SqliteConnection;
use sqlx::Connection;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    // 连接内存数据库
    let mut conn = SqliteConnection::connect("sqlite::memory:").await?;
    
    // 创建表
    sqlx::query(
        "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"
    )
    .execute(&mut conn)
    .await?;
    
    // 插入数据
    sqlx::query("INSERT INTO users (name) VALUES (?)")
        .bind("Alice")
        .execute(&mut conn)
        .await?;
    
    // 查询数据
    let rows = sqlx::query("SELECT id, name FROM users")
        .fetch_all(&mut conn)
        .await?;
    
    for row in rows {
        let name: String = row.get("name");
        println!("用户:{}", name);
    }
    
    Ok(())
}

完整示例: sqlx_sample.rs


原理解析

SQLx 特性

SQLx 是一个异步数据库库

  • ✅ 编译时 SQL 验证(可选)
  • ✅ 类型安全查询
  • ✅ 异步执行
  • ✅ 支持多种数据库(SQLite, MySQL, PostgreSQL)

连接数据库

连接字符串

// SQLite 内存数据库
let conn = SqliteConnection::connect("sqlite::memory:").await?;

// SQLite 文件数据库
let conn = SqliteConnection::connect("sqlite:database.db").await?;

// MySQL
let conn = MySqlConnection::connect("mysql://user:pass@localhost/dbname").await?;

// PostgreSQL
let conn = PgConnection::connect("postgres://user:pass@localhost/dbname").await?;

执行 SQL 查询

query 方法

// 执行无返回值的 SQL
sqlx::query("DELETE FROM users WHERE id = ?")
    .bind(user_id)
    .execute(&mut conn)
    .await?;

// 执行并获取影响的行数
let result = sqlx::query("UPDATE users SET name = ? WHERE id = ?")
    .bind(new_name)
    .bind(user_id)
    .execute(&mut conn)
    .await?;

println!("影响了 {} 行", result.rows_affected());

查询并获取结果

fetch_all 获取所有行

let rows = sqlx::query("SELECT id, name, email FROM users")
    .fetch_all(&mut conn)
    .await?;

for row in rows {
    let id: i64 = row.get("id");
    let name: String = row.get("name");
    let email: String = row.get("email");
    
    println!("ID: {}, Name: {}, Email: {}", id, name, email);
}

fetch_one 获取单行

let row = sqlx::query("SELECT * FROM users WHERE id = ?")
    .bind(user_id)
    .fetch_one(&mut conn)
    .await?;

映射到结构体

定义结构体

use sqlx::FromRow;

#[derive(Debug, FromRow)]
struct User {
    id: i64,
    name: String,
    email: String,
}

查询并映射

let users: Vec<User> = sqlx::query_as::<_, User>("SELECT id, name, email FROM users")
    .fetch_all(&mut conn)
    .await?;

for user in users {
    println!("用户:{:?}", user);
}

连接池

创建连接池

use sqlx::sqlite::SqlitePool;

// 创建连接池
let pool = SqlitePool::connect("sqlite:database.db").await?;

// 使用连接池(自动管理连接)
let users = sqlx::query_as::<_, User>("SELECT * FROM users")
    .fetch_all(&pool)
    .await?;

连接池优势

  • 自动管理连接
  • 控制最大连接数
  • 连接复用,提高性能

常见错误

错误 1: SQL 语法错误

sqlx::query("SELEC * FROM users")  // ❌ SELEC 拼写错误
    .fetch_all(&mut conn)
    .await?;

错误信息:

SQL logic error: near "SELEC": syntax error

修复方法:

sqlx::query("SELECT * FROM users")  // ✅ 正确拼写

错误 2: 参数绑定类型不匹配

sqlx::query("SELECT * FROM users WHERE id = ?")
    .bind("123")  // ❌ id 是 INTEGER,绑定了字符串
    .fetch_all(&mut conn)
    .await?;

修复方法:

sqlx::query("SELECT * FROM users WHERE id = ?")
    .bind(123i64)  // ✅ 绑定正确的类型
    .fetch_all(&mut conn)
    .await?;

错误 3: 忘记 await

let rows = sqlx::query("SELECT * FROM users")
    .fetch_all(&mut conn);  // ❌ 忘记 .await

// rows 是 Future,不是结果

修复方法:

let rows = sqlx::query("SELECT * FROM users")
    .fetch_all(&mut conn)
    .await?;  // ✅ 添加 .await

动手练习

练习 1: 创建用户表

use sqlx::sqlite::SqliteConnection;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    let mut conn = SqliteConnection::connect("sqlite::memory:").await?;
    
    // TODO: 创建 users 表
    // 字段:id (INTEGER PRIMARY KEY), name (TEXT), email (TEXT)
    
    Ok(())
}
点击查看答案
sqlx::query(
    "CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT NOT NULL
    )"
)
.execute(&mut conn)
.await?;

练习 2: 插入和查询用户

// TODO: 插入 3 个用户
// TODO: 查询所有用户
// TODO: 打印用户列表
点击查看答案
// 插入
sqlx::query("INSERT INTO users (name, email) VALUES (?, ?)")
    .bind("Alice")
    .bind("alice@example.com")
    .execute(&mut conn)
    .await?;

// 查询
let users = sqlx::query("SELECT name, email FROM users")
    .fetch_all(&mut conn)
    .await?;

for row in users {
    let name: String = row.get("name");
    let email: String = row.get("email");
    println!("{} - {}", name, email);
}

练习 3: 使用结构体映射

#[derive(Debug, sqlx::FromRow)]
struct User {
    // TODO: 定义字段
}

// TODO: 查询并映射到 User 结构体
点击查看答案
#[derive(Debug, sqlx::FromRow)]
struct User {
    id: i64,
    name: String,
    email: String,
}

let users: Vec<User> = sqlx::query_as::<_, User>("SELECT id, name, email FROM users")
    .fetch_all(&mut conn)
    .await?;

for user in users {
    println!("{:?}", user);
}

故障排查 (FAQ)

Q: SQLx 和 Diesel 有什么区别?

A:

  • SQLx: 异步,编译时 SQL 验证,更灵活
  • Diesel: 同步,ORM 风格,类型安全更强
  • 推荐: SQLx(异步性能更好)

Q: 什么时候使用连接池?

A:

  • Web 应用(多用户并发)
  • 高并发场景
  • 需要控制连接数时
// 单连接
let conn = SqliteConnection::connect(url).await?;

// 连接池
let pool = SqlitePool::connect(url).await?;

Q: 如何处理事务?

A:

let mut tx = conn.begin().await?;

sqlx::query("INSERT ...").execute(&mut tx).await?;
sqlx::query("UPDATE ...").execute(&mut tx).await?;

tx.commit().await?;  // 或 tx.rollback().await?;

知识扩展

查询构建器

// 动态构建查询
let mut query = sqlx::query("SELECT * FROM users WHERE 1=1");

if let Some(name) = filter_name {
    query = query.bind(name);
}

if let Some(email) = filter_email {
    query = query.bind(email);
}

let users = query.fetch_all(&pool).await?;

迁移 (Migrations)

// 运行数据库迁移
sqlx::migrate!("./migrations")
    .run(&pool)
    .await?;

性能优化

// 使用 prepare 预编译查询
let query = sqlx::query("SELECT * FROM users WHERE id = ?");
let cached = query.persistent(true);

// 多次执行更高效
cached.bind(1).fetch_one(&pool).await?;
cached.bind(2).fetch_one(&pool).await?;

小结

核心要点

  1. SQLx: 异步数据库库,支持多种数据库
  2. query: 执行 SQL 查询
  3. bind: 参数绑定,防止 SQL 注入
  4. fetch_all/fetch_one: 获取查询结果
  5. FromRow: 自动映射到结构体
  6. 连接池: 管理多个连接,提高性能

关键术语

  • Connection (连接): 数据库连接
  • Pool (连接池): 连接池管理
  • Query (查询): SQL 查询
  • Bind (绑定): 参数绑定
  • FromRow: 结果映射 trait

术语表

English中文
Connection连接
Connection Pool连接池
Query查询
Bind绑定
Transaction事务
Migration迁移

知识检查

快速测验(答案在下方):

  1. query!query_as! 有什么区别?

  2. SQLx 的编译时检查需要什么条件?

  3. 如何处理可选参数?

点击查看答案与解析
  1. query! 返回匿名结构体,query_as! 返回指定类型
  2. 需要数据库连接和 DATABASE_URL 环境变量
  3. 使用 Option<T> 参数,SQL 中用 IS NULL 处理

关键理解: SQLx 在编译时验证 SQL,减少运行时错误。

继续学习

前一章: Tokio 异步运行时
下一章: Diesel ORM

相关章节:

返回: 高级进阶


完整示例: sqlx_sample.rs

Axum Web 框架

开篇故事

想象你开了一家餐厅。你需要:前台接待(路由)、服务员(处理器)、厨房(业务逻辑)、收银台(响应)。Axum 就像这个餐厅的完整管理系统——它帮你组织所有组件,让顾客(请求)得到高效服务。


本章适合谁

如果你想用 Rust 构建 Web 服务(REST API、Web 应用),本章适合你。Axum 是 Tokio 团队开发的 Web 框架,以类型安全、高性能、易用性著称。


你会学到什么

完成本章后,你可以:

  1. 创建 Axum Web 应用
  2. 定义路由和处理器
  3. 处理 JSON 请求和响应
  4. 实现优雅的服务器关闭
  5. 使用中间件和错误处理

前置要求

  • Tokio 异步运行时 - 异步基础
  • Serde 序列化 - JSON 处理
  • 结构体 - 结构体定义

依赖安装

运行以下命令安装所需依赖:

cargo add serde --features derive
cargo add tokio --features full
cargo add axum

第一个例子

最简单的 Axum Web 服务器:

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    // 创建路由
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }));
    
    // 监听端口
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    
    println!("服务器运行在 http://localhost:3000");
    
    // 启动服务器
    axum::serve(listener, app).await.unwrap();
}

💡 注意:此代码需要服务端运行。请使用 cargo run --bin axum_sample 进行完整测试,并使用浏览器或 HTTP 客户端访问。

完整示例: axum_sample.rs


原理解析

Axum 核心概念

Axum 是一个 Web 框架

  • ✅ 基于 Tokio 异步运行时
  • ✅ 类型安全的路由
  • ✅ 内置 JSON 支持
  • ✅ 中间件支持
  • ✅ 优雅关闭

创建应用

Router 定义路由

use axum::Router;

let app = Router::new()
    .route("/", get(root_handler))
    .route("/users", get(list_users))
    .route("/users/:id", get(get_user));

路由处理器

简单处理器

use axum::response::IntoResponse;

async fn root() -> impl IntoResponse {
    "Hello, World!"
}

async fn hello(name: &str) -> String {
    format!("Hello, {}!", name)
}

带参数的处理器

use axum::extract::Path;

async fn get_user(Path(user_id): Path<u32>) -> String {
    format!("获取用户 {}", user_id)
}

JSON 响应

定义数据结构

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    username: String,
}

返回 JSON

use axum::Json;

async fn get_user() -> Json<User> {
    Json(User {
        id: 1337,
        username: "Wee".to_string(),
    })
}

JSON 请求

接收 JSON 数据

use axum::Json;
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    username: String,
    email: String,
}

async fn create_user(
    Json(payload): Json<CreateUser>
) -> impl IntoResponse {
    // 处理创建用户逻辑
    let user = User {
        id: 1,
        username: payload.username,
    };
    
    (StatusCode::CREATED, Json(user))
}

优雅关闭

实现优雅关闭

use tokio::signal;

async fn shutdown_signal() {
    let ctrl_c = async {
        signal::ctrl_c()
            .await
            .expect("failed to install Ctrl+C handler");
    };

    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("failed to install signal handler")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }

    println!("收到关闭信号,优雅关闭中...");
}

// 使用
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app)
    .with_graceful_shutdown(shutdown_signal())
    .await
    .unwrap();

💡 注意:此代码需要服务端运行。请使用完整的服务器程序进行测试。

完整示例

多路由应用

use axum::{
    routing::{get, post},
    Json, Router,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    username: String,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(root))
        .route("/users", get(list_users).post(create_user))
        .route("/users/:id", get(get_user));
    
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    
    println!("服务器运行在 http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

async fn root() -> &'static str {
    "Hello, World!"
}

async fn list_users() -> Json<Vec<User>> {
    Json(vec![
        User { id: 1, username: "Alice".to_string() },
        User { id: 2, username: "Bob".to_string() },
    ])
}

async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
    Json(User {
        id: 1,
        username: payload.username,
    })
}

async fn get_user(Path(id): Path<u32>) -> Json<User> {
    Json(User {
        id,
        username: "User".to_string(),
    })
}

💡 注意:此代码需要服务端运行。请使用 cargo run 启动服务器,并使用 HTTP 客户端(如 curl 或浏览器)访问各路由进行测试。


常见错误

错误 1: 忘记使用 #[tokio::main]

fn main() {  // ❌ 忘记 #[tokio::main]
    let app = Router::new();
    // ...
}

错误信息:

error[E0308]: mismatched types

修复方法:

#[tokio::main]  // ✅ 添加异步运行时
async fn main() {
    // ...
}

错误 2: 路由路径不匹配

// 定义路由
.route("/users/:id", get(get_user))

// 访问 /users/ 或 /users/abc
// ❌ 404 Not Found

修复方法:

  • 确保 URL 与定义匹配
  • :id 必须是数字(如果定义为 Path<u32>

错误 3: JSON 解析失败

// 发送错误的 JSON
POST /users
{"user_name": "Alice"}  // ❌ 字段名错误

// 期望的 JSON
{"username": "Alice"}  // ✅

错误信息:

422 Unprocessable Entity
missing field `username`

动手练习

练习 1: 创建简单路由

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    // TODO: 创建 Router
    // TODO: 添加 "/" 路由,返回 "Hello!"
    // TODO: 监听 3000 端口
}
点击查看答案
use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello!" }));
    
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    
    axum::serve(listener, app).await.unwrap();
}

💡 注意:此代码需要服务端运行。请使用 cargo run 启动服务器,并使用 HTTP 客户端访问进行测试。

练习 2: 实现用户 API

#[derive(Serialize, Deserialize)]
struct User {
    // TODO: 定义字段 (id, username)
}

// TODO: 实现 list_users 处理器
// TODO: 实现 create_user 处理器
点击查看答案
#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    username: String,
}

async fn list_users() -> Json<Vec<User>> {
    Json(vec![
        User { id: 1, username: "Alice".to_string() },
    ])
}

async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
    Json(User {
        id: 1,
        username: payload.username,
    })
}

练习 3: 添加路径参数

use axum::extract::Path;

// TODO: 实现 get_user 处理器
// 接收用户 ID 参数
// 返回用户信息
点击查看答案
use axum::extract::Path;

async fn get_user(Path(user_id): Path<u32>) -> Json<User> {
    Json(User {
        id: user_id,
        username: format!("User{}", user_id),
    })
}

故障排查 (FAQ)

Q: Axum 和 Actix-web 有什么区别?

A:

  • Axum: Tokio 团队开发,类型安全,更现代
  • Actix-web: 性能最优,生态成熟
  • 推荐: Axum(类型安全更好)

Q: 如何处理 CORS?

A:

use tower_http::cors::{CorsLayer, Any};

let app = Router::new()
    .route("/", get(handler))
    .layer(CorsLayer::new().allow_origin(Any));

Q: 如何添加中间件?

A:

use axum::middleware;

let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(logging_middleware));

知识扩展

状态共享

use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(Clone)]
struct AppState {
    users: Arc<RwLock<Vec<User>>>,
}

let state = AppState {
    users: Arc::new(RwLock::new(vec![])),
};

let app = Router::new()
    .route("/users", get(list_users))
    .with_state(state);

错误处理

use axum::http::StatusCode;

async fn handler() -> Result<Json<User>, StatusCode> {
    let user = get_user_from_db().await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    Ok(Json(user))
}

请求体验证

use axum::extract::rejection::JsonRejection;

async fn handler(
    result: Result<Json<CreateUser>, JsonRejection>
) -> impl IntoResponse {
    match result {
        Ok(Json(payload)) => create_user(payload).await,
        Err(rejection) => {
            (StatusCode::BAD_REQUEST, rejection.body_text())
        }
    }
}

小结

核心要点

  1. Axum: 类型安全的 Web 框架
  2. Router: 定义路由
  3. Handler: 处理请求的函数
  4. Json: 自动序列化/反序列化
  5. Path: 提取路径参数
  6. 优雅关闭: 处理关闭信号

关键术语

  • Router (路由器): 路由管理
  • Handler (处理器): 请求处理函数
  • Middleware (中间件): 请求/响应拦截器
  • Extractor (提取器): 从请求提取数据
  • Graceful Shutdown (优雅关闭): 优雅处理关闭

术语表

English中文
Router路由器
Handler处理器
Middleware中间件
Extractor提取器
Graceful Shutdown优雅关闭
Route路由

知识检查

快速测验(答案在下方):

  1. Axum 和 Hyper 是什么关系?

  2. 路由参数如何提取?

  3. 中间件在 Axum 中如何实现?

点击查看答案与解析
  1. Axum 构建在 Hyper 之上,提供更高级的 API
  2. 使用 Path<T> 提取器,T 需要实现 Deserialize
  3. 使用 tower::Serviceaxum::middleware

关键理解: Axum 是 Tokio 团队开发的 Web 框架。

继续学习

前一章: Tokio 异步运行时
下一章: HTTP 库

相关章节:

  • Tokio 异步运行时
  • HTTP 库
  • 序列化

返回: 高级进阶


完整示例: axum_sample.rs

Hyper HTTP 库

开篇故事

想象你要建一家餐厅。Axum 是全套服务(前台、服务员、厨房),而 Hyper 只是厨房——它处理 HTTP 协议的核心部分,让你能构建自己的 Web 框架。Hyper 是 Rust 生态中许多 Web 框架的基础。


本章适合谁

如果你想深入理解 HTTP 协议底层,或想构建自己的 Web 框架,本章适合你。Hyper 是低级 HTTP 库,提供最大的灵活性。


你会学到什么

完成本章后,你可以:

  1. 理解 HTTP 请求和响应
  2. 创建 Hyper 服务器
  3. 处理请求路由
  4. 处理请求体和响应体
  5. 实现自定义服务

前置要求

  • Tokio 异步运行时 - 异步基础
  • HTTP 基础 - HTTP 概念(可选)

依赖安装

运行以下命令安装所需依赖:

cargo add serde_json
cargo add tokio --features full
cargo add hyper --features full

第一个例子

最简单的 Hyper 服务器:

use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use tokio::net::TcpListener;

async fn handle_request(
    req: Request<Incoming>
) -> Result<Response<String>, hyper::Error> {
    Ok(Response::new("Hello, World!".to_string()))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "127.0.0.1:3000";
    let listener = TcpListener::bind(addr).await?;
    
    println!("服务器运行在 http://{}", addr);
    
    loop {
        let (stream, _) = listener.accept().await?;
        
        tokio::task::spawn(async move {
            let io = hyper_util::rt::TokioIo::new(stream);
            
            http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await
        });
    }
}

💡 注意:此代码需要服务端运行。请使用 cargo run --bin hyper_sample 进行完整测试,并使用 HTTP 客户端访问。

完整示例: hyper_sample.rs


原理解析

Hyper 特性

Hyper 是一个 HTTP 库

  • ✅ 低级别 HTTP 实现
  • ✅ 高性能
  • ✅ 异步支持
  • ✅ 可构建 Web 框架

HTTP 请求和响应

Request 结构

use hyper::{Request, Method};

let req = Request::builder()
    .method(Method::GET)
    .uri("/hello")
    .body(())
    .unwrap();

Response 结构

use hyper::{Response, StatusCode};

let resp = Response::builder()
    .status(StatusCode::OK)
    .body("Hello, World!")
    .unwrap();

服务函数

service_fn 处理请求

use hyper::service::service_fn;

async fn handle(
    req: Request<Incoming>
) -> Result<Response<String>, hyper::Error> {
    Ok(Response::new("Hello!".to_string()))
}

// 使用
service_fn(handle)

路由

手动路由

async fn router(
    req: Request<Incoming>
) -> Result<Response<String>, hyper::Error> {
    match req.uri().path() {
        "/hello" => Ok(Response::new("Hello!".to_string())),
        "/echo" => Ok(Response::new("Echo!".to_string())),
        _ => Ok(Response::builder()
            .status(404)
            .body("Not Found".to_string())
            .unwrap()),
    }
}

请求体处理

读取请求体

use hyper::body::Bytes;
use http_body_util::BodyExt;

async fn echo(
    req: Request<Incoming>
) -> Result<Response<String>, hyper::Error> {
    // 收集整个请求体
    let whole_body = req.collect().await?.to_bytes();
    
    Ok(Response::new(format!(
        "Echo: {}",
        String::from_utf8_lossy(&whole_body)
    )))
}

响应体

使用 Full 响应体

use http_body_util::Full;
use hyper::body::Bytes;

let body = Full::new(Bytes::from("Hello, World!"));
let response = Response::new(body);

使用 Empty 响应体

use http_body_util::Empty;

let body = Empty::<Bytes>::new();
let response = Response::new(body);

完整服务器示例

use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use tokio::net::TcpListener;

async fn handle(
    req: Request<Incoming>
) -> Result<Response<Full<Bytes>>, hyper::Error> {
    match req.uri().path() {
        "/hello" => Ok(Response::new(Full::new(Bytes::from("Hello!")))),
        "/echo" => {
            let body = req.collect().await?.to_bytes();
            Ok(Response::new(Full::new(body)))
        }
        _ => Ok(Response::builder()
            .status(StatusCode::NOT_FOUND)
            .body(Full::new(Bytes::from("Not Found")))
            .unwrap()),
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "127.0.0.1:3000";
    let listener = TcpListener::bind(addr).await?;
    
    println!("服务器运行在 http://{}", addr);
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = hyper_util::rt::TokioIo::new(stream);
        
        tokio::task::spawn(async move {
            http1::Builder::new()
                .serve_connection(io, service_fn(handle))
                .await
        });
    }
}

💡 注意:此代码需要服务端运行。请使用 cargo run 启动服务器,并使用 HTTP 客户端访问进行测试。


常见错误

错误 1: 忘记使用 Tokio

fn main() {  // ❌ 忘记 #[tokio::main]
    let listener = TcpListener::bind("127.0.0.1:3000");
    // ...
}

错误信息:

error[E0308]: mismatched types

修复方法:

#[tokio::main]  // ✅ 添加异步运行时
async fn main() {
    // ...
}

错误 2: 类型不匹配

// ❌ 错误的响应体类型
Response::new("Hello")  // 期望 Body 类型

修复方法:

Response::new(Full::new(Bytes::from("Hello")))  // ✅ 正确的类型

错误 3: 忘记收集请求体

async fn handler(req: Request<Incoming>) {
    let body = req.body();  // ❌ 这是 Incoming 类型,不是数据
}

修复方法:

async fn handler(req: Request<Incoming>) {
    let body = req.collect().await?.to_bytes();  // ✅ 收集并转换为 Bytes
}

动手练习

练习 1: 创建简单服务器

use hyper::server::conn::http1;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    // TODO: 绑定 3000 端口
    // TODO: 接受连接
    // TODO: 处理请求
}
点击查看答案
let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();

loop {
    let (stream, _) = listener.accept().await.unwrap();
    let io = hyper_util::rt::TokioIo::new(stream);
    
    tokio::task::spawn(async move {
        http1::Builder::new()
            .serve_connection(io, service_fn(handle))
            .await
    });
}

💡 注意:此代码需要服务端运行。请使用 cargo run 启动服务器,并使用 HTTP 客户端访问进行测试。

练习 2: 实现路由

async fn handle(req: Request<Incoming>) {
    // TODO: 根据路径路由
    // "/" → 返回 "Home"
    // "/about" → 返回 "About"
    // 其他 → 返回 404
}
点击查看答案
match req.uri().path() {
    "/" => Response::new(Full::new(Bytes::from("Home"))),
    "/about" => Response::new(Full::new(Bytes::from("About"))),
    _ => Response::builder()
        .status(404)
        .body(Full::new(Bytes::from("Not Found")))
        .unwrap(),
}

练习 3: 实现 Echo 服务

async fn echo(req: Request<Incoming>) {
    // TODO: 读取请求体
    // TODO: 返回请求体内容
}
点击查看答案
let body = req.collect().await?.to_bytes();
Ok(Response::new(Full::new(body)))

故障排查 (FAQ)

Q: Hyper 和 Axum 有什么区别?

A:

  • Hyper: 低级 HTTP 库,最大灵活性
  • Axum: 基于 Hyper 的高级框架,更易用
  • 推荐: Axum(除非需要底层控制)

Q: 如何处理 JSON?

A:

use serde_json;

let json = serde_json::to_string(&data)?;
Response::new(Full::new(Bytes::from(json)))

Q: 如何添加中间件?

A:

use tower::ServiceBuilder;

let service = ServiceBuilder::new()
    .layer(LoggingLayer)
    .service(service_fn(handle));

知识扩展

HTTPS 支持

use tokio_rustls::TlsAcceptor;

let acceptor = TlsAcceptor::from(config);
let stream = acceptor.accept(stream).await?;

WebSocket 支持

use tokio_tungstenite::WebSocketStream;

// 升级 HTTP 连接到 WebSocket
let ws_stream = tokio_tungstenite::accept_async(stream).await?;

连接池

use hyper_util::client::legacy::Client;

let client = Client::builder(hyper_util::rt::TokioExecutor::new())
    .build_http();

let resp = client.get(uri).await?;

小结

核心要点

  1. Hyper: 低级 HTTP 库
  2. Request/Response: HTTP 请求和响应
  3. service_fn: 请求处理函数
  4. 路由: 手动匹配路径
  5. Body: 请求体和响应体处理
  6. 异步: 使用 Tokio 运行时

关键术语

  • HTTP: 超文本传输协议
  • Request: HTTP 请求
  • Response: HTTP 响应
  • Body: 请求/响应体
  • Service: 服务处理函数

术语表

English中文
HTTP超文本传输协议
Request请求
Response响应
Body体/主体
Service服务
Route路由

知识检查

快速测验(答案在下方):

  1. Hyper 和 Tokio 是什么关系?

  2. Service trait 的作用是什么?

  3. 什么时候应该直接使用 Hyper 而不是 Axum?

点击查看答案与解析
  1. Hyper 是基于 Tokio 的 HTTP 库
  2. Service 是处理请求/响应的抽象
  3. 需要极致性能控制、自定义 HTTP 行为时

关键理解: Hyper 是底层 HTTP 库,Axum 是高级框架。

继续学习

前一章: Axum Web 框架
下一章: JSON 序列化

相关章节:

返回: 高级进阶


完整示例: hyper_sample.rs

Ollama AI 集成

开篇故事

想象你要和一个 AI 聊天。传统方式是:调用复杂的 API → 处理响应 → 显示结果。Ollama 就像是:本地 AI 助手——在本地运行大语言模型,简单快速地集成 AI 功能。


本章适合谁

如果你想在 Rust 程序中集成 AI 功能(聊天机器人、文本生成),本章适合你。Ollama 是本地运行大语言模型的简单方式。


你会学到什么

完成本章后,你可以:

  1. 理解 Ollama 概念
  2. 连接 Ollama 服务
  3. 发送生成请求
  4. 处理 AI 响应
  5. 创建聊天机器人

前置要求

  • 异步编程 - async/await 基础
  • Tokio 运行时 - Tokio 基础
  • 错误处理 - 错误处理基础

依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add ollama-rs
cargo add futures

第一个例子

最简单的 Ollama 使用:

use ollama_rs::{
    generation::completion::request::GenerationRequest,
    Ollama,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 连接本地 Ollama 服务(默认 localhost:11434)
    let ollama = Ollama::default();
    
    // 指定模型和提示
    let model = "llama3.2:latest".to_string();
    let prompt = "Why is the sky blue?".to_string();
    
    // 发送生成请求
    let res = ollama.generate(
        GenerationRequest::new(model, prompt)
    ).await?;
    
    // 打印响应
    if let Ok(res) = res {
        println!("{}", res.response);
    }
    
    Ok(())
}

💡 注意:此代码需要 Ollama 服务运行。请先安装并启动 Ollama (ollama serve),然后使用 cargo run --bin ollama_sample 进行完整测试。

完整示例: ollama_sample.rs


原理解析

Ollama 特性

Ollama 是本地 AI 运行库

  • ✅ 本地运行大语言模型
  • ✅ 简单易用的 API
  • ✅ 支持多种模型
  • ✅ 异步支持

连接 Ollama

使用默认配置

use ollama_rs::Ollama;

// 默认连接到 localhost:11434
let ollama = Ollama::default();

使用自定义配置

// 自定义主机和端口
let ollama = Ollama::new(
    "http://localhost".to_string(),
    11434
);

生成请求

使用 GenerationRequest

use ollama_rs::generation::completion::request::GenerationRequest;

let model = "llama3.2:latest".to_string();
let prompt = "解释量子力学".to_string();

let request = GenerationRequest::new(model, prompt);
let response = ollama.generate(request).await?;

💡 注意:此代码需要 Ollama 服务运行并已下载相应模型。请先使用 ollama pull llama3.2:latest 下载模型。

处理响应

解析生成结果

let res = ollama.generate(request).await?;

if let Ok(res) = res {
    println!("AI 响应:{}", res.response);
} else {
    eprintln!("生成失败");
}

💡 注意:此代码需要 Ollama 服务运行。请确保 Ollama 服务已启动。

聊天机器人

实现简单聊天

async fn chatbot() -> Result<(), Box<dyn std::error::Error>> {
    let ollama = Ollama::default();
    let model = "llama3.2:latest".to_string();
    
    loop {
        print!("你:");
        std::io::stdout().flush()?;
        
        let mut input = String::new();
        std::io::stdin().read_line(&mut input)?;
        
        if input.trim() == "exit" {
            break;
        }
        
        let response = ollama.generate(
            GenerationRequest::new(model.clone(), input.trim().to_string())
        ).await?;
        
        println!("AI: {}", response.response);
    }
    
    Ok(())
}

💡 注意:此代码需要 Ollama 服务运行。请先启动 Ollama 服务并下载模型。


常见错误

错误 1: Ollama 服务未运行

let ollama = Ollama::default();
let res = ollama.generate(request).await?;
// ❌ 如果 Ollama 服务未运行会失败

错误信息:

error sending request for url

修复方法:

# 启动 Ollama 服务
ollama serve

错误 2: 模型未下载

let model = "llama3.2:latest".to_string();
// ❌ 如果模型未下载会失败

错误信息:

model 'llama3.2:latest' not found

修复方法:

# 下载模型
ollama pull llama3.2:latest

错误 3: 忘记 await

let res = ollama.generate(request);  // ❌ 忘记 .await
println!("{}", res.response);

错误信息:

no field `response` on type `impl Future`

修复方法:

let res = ollama.generate(request).await?;  // ✅ 添加 .await

动手练习

练习 1: 连接 Ollama

use ollama_rs::Ollama;

#[tokio::main]
async fn main() {
    // TODO: 创建 Ollama 实例
    // TODO: 打印连接信息
}
点击查看答案
let ollama = Ollama::default();
println!("已连接到 Ollama 服务");

练习 2: 发送问题

use ollama_rs::{
    generation::completion::request::GenerationRequest,
    Ollama,
};

#[tokio::main]
async fn main() {
    let ollama = Ollama::default();
    
    // TODO: 创建请求
    // TODO: 发送问题 "什么是 Rust?"
    // TODO: 打印回答
}
点击查看答案
let model = "llama3.2:latest".to_string();
let prompt = "什么是 Rust?".to_string();

let res = ollama.generate(
    GenerationRequest::new(model, prompt)
).await.unwrap();

println!("{}", res.response);

💡 注意:此代码需要 Ollama 服务运行并已下载相应模型。

练习 3: 创建聊天机器人

async fn chatbot() {
    let ollama = Ollama::default();
    let model = "llama3.2:latest".to_string();
    
    // TODO: 循环读取用户输入
    // TODO: 发送到 Ollama
    // TODO: 打印响应
    // TODO: 输入"exit"退出
}
点击查看答案
loop {
    print!("你:");
    std::io::stdout().flush().unwrap();
    
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).unwrap();
    
    if input.trim() == "exit" {
        break;
    }
    
    let response = ollama.generate(
        GenerationRequest::new(model.clone(), input.trim().to_string())
    ).await.unwrap();
    
    println!("AI: {}", response.response);
}

💡 注意:此代码需要 Ollama 服务运行。请先启动 Ollama 服务并下载模型。


故障排查 (FAQ)

Q: Ollama 和 OpenAI API 有什么区别?

A:

  • Ollama: 本地运行,免费,隐私好
  • OpenAI: 云端 API,付费,功能强
  • 选择: 根据需求选择

Q: 支持哪些模型?

A:

  • Llama 3.2
  • Llama 3
  • Mistral
  • Gemma
  • 等等(查看 ollama.ai 获取完整列表)

Q: 如何提高响应速度?

A:

  • 使用较小的模型
  • 减少提示长度
  • 使用 GPU 加速

知识扩展

流式响应

use ollama_rs::generation::completion::request::GenerationRequest;

let mut stream = ollama.generate_stream(
    GenerationRequest::new(model, prompt)
).await?;

while let Some(chunk) = stream.next().await {
    print!("{}", chunk.response);
    std::io::stdout().flush()?;
}

💡 注意:此代码需要 Ollama 服务运行。请先启动 Ollama 服务。

多轮对话

let mut context = Vec::new();

// 第一轮
let response = ollama.generate(
    GenerationRequest::new(model.clone(), "你好")
        .with_context(context.clone())
).await?;
context.push(response.context);

// 第二轮(带上下文)
let response = ollama.generate(
    GenerationRequest::new(model, "继续")
        .with_context(context)
).await?;

💡 注意:此代码需要 Ollama 服务运行。请先启动 Ollama 服务。

自定义参数

let request = GenerationRequest::new(model, prompt)
    .temperature(0.8)  // 创造性
    .top_p(0.9)        // 采样
    .num_predict(100); // 最大 token 数

小结

核心要点

  1. Ollama: 本地 AI 运行库
  2. 默认连接: localhost:11434
  3. GenerationRequest: 生成请求
  4. 异步支持: 使用 async/await
  5. 聊天机器人: 简单实现

关键术语

  • Ollama: AI 运行平台
  • Generation: 生成
  • Model: 模型
  • Prompt: 提示

术语表

English中文
OllamaOllama
Generation生成
Model模型
Prompt提示
Chatbot聊天机器人

知识检查

快速测验(答案在下方):

  1. Ollama 是什么?

  2. 如何在 Rust 中调用 Ollama API?

  3. 本地 AI 集成的应用场景有哪些?

点击查看答案与解析
  1. Ollama 是本地运行大语言模型的工具
  2. 使用 HTTP 客户端(如 reqwest)调用 Ollama 的 REST API
  3. 智能助手、代码生成、文本分析、摘要

关键理解: 本地 AI 集成保护隐私,无需云端依赖。

继续学习

前一章: 对象存储
下一章: 进程管理

相关章节:

  • 对象存储
  • 异步编程
  • Tokio 运行时

返回: 高级进阶


完整示例: ollama_sample.rs

gRPC 服务

开篇故事

想象你在开发一个微服务系统。服务 A 需要调用服务 B 的函数,如果直接调用,两个服务必须部署在同一台机器上,使用相同的编程语言。但现实是:服务 A 用 Rust 编写,服务 B 用 Go 编写,它们运行在不同的服务器上。

gRPC 就像是一个"通用翻译器"——你定义服务接口(使用 Protocol Buffers),gRPC 自动生成客户端和服务端代码,让不同语言、不同机器的服务可以像本地调用一样通信。


本章适合谁

如果你想学习:

  • 如何使用 gRPC 构建跨服务通信
  • tonic 框架在 Rust 中的使用
  • 如何定义和使用 Protocol Buffers

本章适合你。gRPC 是现代微服务架构的核心技术。


你会学到什么

完成本章后,你可以:

  1. 理解 gRPC 的核心概念(服务定义、Protocol Buffers、流式通信)
  2. 使用 tonic 创建 gRPC 服务端
  3. 使用 tonic 创建 gRPC 客户端
  4. 实现 Unary、Server Streaming、Client Streaming、Bidirectional Streaming
  5. 使用 clap 解析命令行参数

前置要求

  • 异步编程 - async/await 基础
  • Tokio - Tokio 异步运行时
  • 理解微服务基本概念

依赖安装

运行以下命令安装所需依赖:

cargo add tonic --features full
cargo add prost
cargo add clap --features derive
cargo add tokio --features full
cargo add anyhow

第一个例子

最简单的 gRPC Hello 服务:

服务端 (grpc_hello_server.rs):

use awesome::services::tonic_hello_server;
use clap::Parser;

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    #[arg(default_value = "127.0.0.1", long)]
    host: String,
    #[arg(default_value = "9001", short, long)]
    port: u32,
}

fn main() {
    let opts = Args::parse();
    println!("启动 gRPC 服务器: {}:{}", opts.host, opts.port);
    tonic_hello_server::hello_server(&opts.host, opts.port);
}

💡 注意:此代码需要服务端和客户端配合运行。请使用 cargo run --bin grpc_hello_server 进行完整测试。

客户端 (grpc_hello_client.rs):

use awesome::services::tonic_hello_client;
use clap::Parser;

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    #[clap(default_value = "http://127.0.0.1:9001", long)]
    url: String,
}

fn main() {
    let opts = Args::parse();
    println!("连接到 gRPC 服务器: {}", opts.url);
    tonic_hello_client::hello_client(opts.url);
}

💡 注意:此代码需要服务端和客户端配合运行。请使用 cargo run --bin grpc_hello_client 进行完整测试。

运行方式:

# 先启动服务器
cargo run --bin grpc_hello_server

# 再启动客户端
cargo run --bin grpc_hello_client

完整示例:


原理解析

gRPC 架构概览

┌─────────────────────────────────────────────────────────────────────┐
│                          gRPC 架构                                  │
│                                                                      │
│   ┌──────────────┐         ┌──────────────┐         ┌──────────────┐│
│   │   Client     │         │   Protocol   │         │   Server     ││
│   │  (客户端)     │────────→│   Buffers    │←────────│  (服务端)     ││
│   │              │  HTTP/2  │  (序列化)     │  HTTP/2  │              ││
│   └──────────────┘         └──────────────┘         └──────────────┘│
│                                                                      │
│   通信模式:                                                            │
│   1. Unary RPC         - 单次请求/响应                                 │
│   2. Server Streaming  - 一次请求,多次响应                            │
│   3. Client Streaming  - 多次请求,一次响应                            │
│   4. Bidirectional     - 双向流式通信                                  │
└─────────────────────────────────────────────────────────────────────┘

gRPC 四种通信模式

1. Unary RPC(单次请求/响应)

Client → Request → Server
Client ← Response ← Server

2. Server Streaming(服务端流式)

Client → Request → Server
Client ← Response1 ← Server
Client ← Response2 ← Server
Client ← Response3 ← Server

3. Client Streaming(客户端流式)

Client → Request1 → Server
Client → Request2 → Server
Client → Request3 → Server
Client ← Response ← Server

4. Bidirectional Streaming(双向流式)

Client → Request1 → Server → Response1 → Client
Client → Request2 → Server → Response2 → Client
Client → Request3 → Server → Response3 → Client

Protocol Buffers 基础

// 定义服务
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
    rpc SayHelloStream (HelloRequest) returns (stream HelloReply);
}

// 定义消息
message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

tonic 服务端实现

use tonic::{transport::Server, Request, Response, Status};
use tonic::codegen::tokio_stream;

pub struct GreeterService;

#[tonic::async_trait]
impl Greeter for GreeterService {
    async fn say_hello(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloReply>, Status> {
        let name = request.into_inner().name;
        let reply = HelloReply {
            message: format!("Hello, {}!", name),
        };
        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "127.0.0.1:50051".parse()?;
    let greeter = GreeterService;

    println!("gRPC 服务器启动在 {}", addr);

    Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;

    Ok(())
}

💡 注意:此代码需要服务端和客户端配合运行。请使用 cargo run --bin greeter_server 进行完整测试。

tonic 客户端实现

use tonic::transport::Channel;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = GreeterClient::connect("http://127.0.0.1:50051").await?;

    let request = tonic::Request::new(HelloRequest {
        name: "World".into(),
    });

    let response = client.say_hello(request).await?;

    println!("收到响应: {:?}", response.into_inner());

    Ok(())
}

💡 注意:此代码需要服务端配合运行。请先启动服务器,再运行客户端进行完整测试。

生产级 gRPC 服务架构

┌─────────────────────────────────────────────────────────────────────┐
│                    生产级 gRPC 服务架构                               │
│                                                                      │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐           │
│  │   Consul     │    │   gRPC       │    │   Tracing    │           │
│  │  (服务发现)   │←──→│   Server     │←──→│  (链路追踪)   │           │
│  └──────────────┘    └──────────────┘    └──────────────┘           │
│         │                   │                   │                    │
│         │                   │                   │                    │
│  ┌──────┴───────────────────┴───────────────────┴──────┐            │
│  │              Application Framework                  │            │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │            │
│  │  │   Config    │  │  Lifecycle  │  │   Health    │  │            │
│  │  │  (配置管理)  │  │  (生命周期)  │  │  (健康检查)  │  │            │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  │            │
│  └─────────────────────────────────────────────────────┘            │
└─────────────────────────────────────────────────────────────────────┘

常见错误

错误 1: 忘记启动服务器

// ❌ 错误:客户端连接时服务器未启动
let mut client = GreeterClient::connect("http://127.0.0.1:50051").await?;
// 连接失败!

// ✅ 正确:先启动服务器,再连接客户端
// 终端 1: cargo run --bin grpc_hello_server
// 终端 2: cargo run --bin grpc_hello_client

错误 2: 端口冲突

// ❌ 错误:端口已被占用
let addr = "127.0.0.1:9001".parse()?;
// 如果 9001 端口已被其他服务占用,会失败

// ✅ 正确:使用可配置端口
#[derive(Parser, Debug)]
struct Args {
    #[arg(default_value = "9001", short, long)]
    port: u32,
}

错误 3: 未处理连接错误

// ❌ 错误:直接 unwrap
let mut client = GreeterClient::connect("http://127.0.0.1:50051").await.unwrap();

// ✅ 正确:使用 ? 传播错误
let mut client = GreeterClient::connect("http://127.0.0.1:50051").await?;

动手练习

练习 1: 添加新的 gRPC 方法

在 Greeter 服务中添加 SayHelloMultiple 方法,接受一个名字列表并返回多个问候:

// TODO: 在 .proto 文件中定义
// rpc SayHelloMultiple (NamesRequest) returns (stream HelloReply);

// TODO: 实现服务端逻辑
点击查看答案
async fn say_hello_multiple(
    &self,
    request: Request<NamesRequest>,
) -> Result<Response<Self::SayHelloMultipleStream>, Status> {
    let names = request.into_inner().names;
    let output = async_stream::stream! {
        for name in names {
            yield Ok(HelloReply {
                message: format!("Hello, {}!", name),
            });
        }
    };
    Ok(Response::new(Box::pin(output)))
}

小结

核心要点

  1. gRPC 是高性能、跨语言的 RPC 框架
  2. Protocol Buffers 是 gRPC 的接口定义语言
  3. tonic 是 Rust 的 gRPC 实现
  4. 四种通信模式: Unary、Server Streaming、Client Streaming、Bidirectional
  5. clap 用于解析命令行参数

关键术语

English中文说明
gRPCgRPC 远程过程调用高性能 RPC 框架
Protocol Buffers协议缓冲区接口定义语言
tonictonic 框架Rust 的 gRPC 实现
Unary RPC单次请求/响应最简单的通信模式
Streaming流式通信多次请求/响应
Service Discovery服务发现动态查找服务地址

下一步


术语表

English中文
gRPCgRPC 远程过程调用
Protocol Buffers协议缓冲区
Service服务
Method方法
Request请求
Response响应
Streaming流式通信
Unary单次请求/响应
Bidirectional双向流式

完整示例:


继续学习

💡 记住:gRPC 的核心是"定义接口,自动生成代码"。使用 Protocol Buffers 定义服务,tonic 自动生成类型安全的客户端和服务端代码!

序列化基础

开篇故事

想象你要寄快递到不同国家。每个国家有不同的包装要求:有的用纸箱,有的用木箱,有的用塑料袋。但无论你用什么包装,里面的物品都是一样的。

在 Rust 中,Serde 框架就是你的"通用包装系统"——它定义了一套标准的序列化接口,然后针对不同格式(JSON、YAML、TOML、CSV、二进制)提供具体的"包装"实现。你只需定义一次数据结构,就能序列化成任何格式。


本章适合谁

如果你需要在 Rust 程序中序列化/反序列化数据,本章适合你。序列化是 Web API、配置文件、数据存储的基础。


你会学到什么

完成本章后,你可以:

  1. 理解 Serde 框架的工作原理
  2. 使用 #[derive(Serialize, Deserialize)] 自动生成序列化代码
  3. 自定义序列化行为
  4. 使用 Serde 属性控制序列化行为
  5. 处理多种数据格式(JSON、YAML、TOML、CSV)

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add serde --features derive
cargo add serde_json  # JSON 格式
# cargo add serde_yaml  # YAML 格式
# cargo add toml        # TOML 格式
# cargo add csv         # CSV 格式

第一个例子

最简单的序列化示例:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u8,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let person = Person {
        name: String::from("Alice"),
        age: 30,
    };

    // 序列化:结构体 → 字节/字符串
    // 反序列化:字节/字符串 → 结构体
    
    println!("原始数据:{:?}", person);

    Ok(())
}

发生了什么?

  • #[derive(Serialize, Deserialize)] - 自动生成序列化代码
  • Serde 框架不关心具体格式,只负责转换数据

原理解析

1. Serde 架构

Serde 框架
├── serde (核心)
│   ├── Serialize trait
│   └── Deserialize trait
├── serde_derive (宏)
│   ├── #[derive(Serialize)]
│   └── #[derive(Deserialize)]
└── 格式库
    ├── serde_json (JSON)
    ├── serde_yaml (YAML)
    ├── bincode (二进制)
    └── csv (CSV)

关键点

  • serde 定义接口(Serialize / Deserialize trait)
  • serde_derive 自动生成实现(派生宏)
  • 格式库 提供具体格式的序列化/反序列化

2. 序列化流程

Rust 结构体
    ↓
Serialize trait
    ↓
Serializer (JSON/YAML/TOML/CSV)
    ↓
字符串/字节

反序列化流程相反

字符串/字节
    ↓
Deserializer (JSON/YAML/TOML/CSV)
    ↓
Deserialize trait
    ↓
Rust 结构体

3. 自定义序列化

当默认行为不满足需求时,可以自定义:

use serde::{Serialize, Serializer, Deserialize, Deserializer};

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

// 自定义序列化:将 Point 序列化为 [x, y] 数组
impl Serialize for Point {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        use serde::ser::SerializeSeq;
        let mut seq = serializer.serialize_seq(Some(2))?;
        seq.serialize_element(&self.x)?;
        seq.serialize_element(&self.y)?;
        seq.end()
    }
}

// 自定义反序列化:从 [x, y] 数组反序列化为 Point
impl<'de> Deserialize<'de> for Point {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let vec = Vec::<i32>::deserialize(deserializer)?;
        if vec.len() != 2 {
            return Err(serde::de::Error::custom("Expected 2 elements"));
        }
        Ok(Point { x: vec[0], y: vec[1] })
    }
}

4. Serde 属性

Serde 提供丰富的属性控制序列化行为:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    // 序列化时使用不同名称
    #[serde(rename = "user_name")]
    name: String,

    // 如果字段为 None 则跳过
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,

    // 反序列化时提供默认值
    #[serde(default = "default_age")]
    age: u8,

    // 反序列化时如果字段缺失使用默认值
    #[serde(default)]
    active: bool,

    // 将嵌套对象展平到当前层级
    #[serde(flatten)]
    extra: std::collections::HashMap<String, serde_json::Value>,
}

fn default_age() -> u8 {
    18
}

5. 多格式支持

使用 Serde,同一结构体可以序列化成多种格式:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Config {
    host: String,
    port: u16,
    debug: bool,
}

fn main() {
    let config = Config {
        host: "localhost".to_string(),
        port: 8080,
        debug: true,
    };

    // JSON
    let json = serde_json::to_string(&config).unwrap();
    println!("JSON: {}", json);

    // YAML (需要 serde_yaml crate)
    // let yaml = serde_yaml::to_string(&config).unwrap();
    // println!("YAML: {}", yaml);

    // TOML (需要 toml crate)
    // let toml = toml::to_string(&config).unwrap();
    // println!("TOML: {}", toml);
}

常见错误

错误 1: 忘记 derive

// ❌ 错误:没有 derive
struct Person {
    name: String,
}
// serde_json::to_string(&person)?;  // 编译错误!

// ✅ 正确:添加 derive
#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
}

错误 2: 不匹配的类型

// ❌ 错误:JSON 中的 age 是字符串,但结构体定义是数字
let json = r#"{"name": "Alice", "age": "30"}"#;
// let person: Person = serde_json::from_str(json)?;  // 反序列化失败!

// ✅ 正确:类型匹配
let json = r#"{"name": "Alice", "age": 30}"#;

错误 3: 生命周期问题

// ❌ 错误:反序列化引用需要生命周期标注
#[derive(Deserialize)]
struct User {
    name: &str,  // 需要生命周期
}

// ✅ 正确:使用拥有的类型
#[derive(Deserialize)]
struct User {
    name: String,  // 拥有的数据
}

动手练习

练习 1: 配置结构体

创建一个配置结构体,支持从多种格式加载:

// TODO: 定义 Config 结构体
// 字段:host (String), port (u16), debug (bool)
// 使用 Serde 属性提供默认值
点击查看答案
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Config {
    host: String,
    #[serde(default = "default_port")]
    port: u16,
    #[serde(default)]
    debug: bool,
}

fn default_port() -> u16 {
    8080
}

练习 2: 自定义序列化

Duration 类型实现自定义序列化:

// TODO: 实现 Duration 的自定义序列化
// 序列化为 "1h 30m 45s" 格式
// 反序列化从相同格式解析
点击查看答案
use serde::{Serialize, Serializer, Deserialize, Deserializer};

#[derive(Debug)]
struct Duration {
    hours: u32,
    minutes: u32,
    seconds: u32,
}

impl Serialize for Duration {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let s = format!("{}h {}m {}s", self.hours, self.minutes, self.seconds);
        serializer.serialize_str(&s)
    }
}

故障排查

Q: serde_json 和 serde 有什么区别?

A:

  • serde: 序列化/反序列化框架 (trait 定义)
  • serde_json: JSON 格式的具体实现

Q: 如何处理未知字段?

A: 使用 #[serde(deny_unknown_fields)] 拒绝未知字段,或默认忽略。

Q: 如何序列化枚举?

A: 使用 #[serde(tag = "type")] 控制枚举格式。

Q: 如何提高序列化性能?

A: 使用 serde_json::to_vec 而不是 to_string,避免 UTF-8 转换。


知识扩展

性能优化

// 使用 to_vec 而不是 to_string (避免 UTF-8 转换)
let bytes = serde_json::to_vec(&data)?;

// 使用 serde_path_to_error 获取详细错误位置
use serde_path_to_error;

let result: Result<T, _> = serde_path_to_error::deserialize(deserializer);

零拷贝反序列化

use serde::Deserialize;

// 借用字符串,避免复制
#[derive(Deserialize)]
struct User<'a> {
    name: &'a str,
    age: u8,
}

小结

核心要点

  1. Serde 是框架:定义接口,格式库提供实现
  2. #[derive(Serialize, Deserialize)]: 自动生成代码
  3. 自定义序列化: 实现 trait
  4. Serde 属性: 控制序列化行为
  5. 多格式支持: 同一结构体可序列化成多种格式

关键术语

English中文
Serialization序列化
Deserialization反序列化
Derive Macro派生宏
Attribute属性
Serializer序列化器
Deserializer反序列化器

术语表

English中文
Serialization序列化
Deserialization反序列化
Derive Macro派生宏
Attribute属性
Transcode转码
Zero-copy零拷贝

完整示例:src/advance/json_sample.rs


知识检查

快速测验(答案在下方):

  1. #[derive(Serialize, Deserialize)] 做了什么?

  2. 如何处理序列化错误?

  3. serdeserde_json 的区别是什么?

点击查看答案与解析
  1. 自动生成序列化和反序列化代码
  2. 使用 Result<T, serde_json::Error> 处理
  3. serde 是框架,serde_json 是 JSON 实现

关键理解: Serde 是 Rust 序列化的事实标准框架,支持多种格式。

继续学习

💡 记住:Serde 让数据在任何地方都能安全"拆包"!

JSON 序列化

开篇故事

想象你要寄快递。你需要把物品打包成标准格式,贴上标签,才能通过快递系统运输。JSON 序列化就像这个打包过程——把你的 Rust 数据结构转换成标准的 JSON 格式,以便在不同系统间传输。


本章适合谁

如果你需要在 Rust 程序中处理 JSON 数据(读取配置文件、调用 API、存储数据),本章适合你。JSON 是现代编程中最常用的数据交换格式。


你会学到什么

完成本章后,你可以:

  1. 使用 serde_json 解析 JSON 数据
  2. 将 Rust 结构体序列化为 JSON
  3. 处理无类型和有类型的 JSON 数据
  4. 处理 JSON 错误和验证
  5. 自定义序列化和反序列化行为

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add serde --features derive
cargo add serde_json

第一个例子

最简单的 JSON 解析:

use serde_json::{Result, Value};

fn main() -> Result<()> {
    // JSON 字符串
    let data = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;

    // 解析为 Value
    let v: Value = serde_json::from_str(data)?;

    // 访问字段
    println!("姓名:{}", v["name"]);
    println!("电话:{}", v["phones"][0]);

    Ok(())
}

完整示例: json_sample.rs


原理解析

serde 生态系统

serde 是序列化框架

  • ✅ 支持多种格式(JSON, YAML, TOML 等)
  • ✅ 类型安全
  • ✅ 零开销序列化
  • ✅ 派生宏简化使用

无类型 JSON (Value)

适用场景: 未知结构的 JSON 数据

use serde_json::Value;

let json_str = r#"{"name": "Alice", "age": 30}"#;
let v: Value = serde_json::from_str(json_str).unwrap();

// 访问字段
println!("{}", v["name"]);  // "Alice"
println!("{}", v["age"]);   // 30

// 检查类型
assert!(v["name"].is_string());
assert!(v["age"].is_number());

有类型 JSON (Struct)

适用场景: 已知结构的 JSON 数据

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

// JSON → Struct (反序列化)
let json_str = r#"{"name":"John","age":43,"phones":["123","456"]}"#;
let person: Person = serde_json::from_str(json_str).unwrap();

// Struct → JSON (序列化)
let json = serde_json::to_string(&person).unwrap();

错误处理

use serde_json::{Result, Error};

fn parse_json(json_str: &str) -> Result<()> {
    match serde_json::from_str::<Value>(json_str) {
        Ok(value) => {
            println!("解析成功:{}", value);
            Ok(())
        }
        Err(e) => {
            eprintln!("解析失败:{}", e);
            Err(e)
        }
    }
}

美化输出

let person = Person {
    name: "Alice".to_string(),
    age: 30,
    phones: vec!["123".to_string()],
};

// 紧凑格式
let compact = serde_json::to_string(&person).unwrap();
// {"name":"Alice","age":30,"phones":["123"]}

// 美化格式
let pretty = serde_json::to_string_pretty(&person).unwrap();
/*
{
  "name": "Alice",
  "age": 30,
  "phones": [
    "123"
  ]
}
*/

自定义序列化

use serde::{Serialize, Serializer};

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

// 自定义序列化
impl Serialize for Point {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&format!("({}, {})", self.x, self.y))
    }
}

常见错误

错误 1: 字段不匹配

#[derive(Deserialize)]
struct Person {
    name: String,
    age: u8,
}

let json = r#"{"name": "Alice"}"#;  // ❌ 缺少 age 字段
let person: Person = serde_json::from_str(json).unwrap();

错误信息:

missing field `age`

修复方法:

#[derive(Deserialize)]
struct Person {
    name: String,
    #[serde(default)]  // ✅ 提供默认值
    age: u8,
}

错误 2: 类型不匹配

#[derive(Deserialize)]
struct Person {
    age: u8,
}

let json = r#"{"age": "twenty"}"#;  // ❌ 期望数字,得到字符串
let person: Person = serde_json::from_str(json).unwrap();

错误信息:

invalid type: string "twenty", expected u8

修复方法:

let json = r#"{"age": 20}"#;  // ✅ 正确的类型

错误 3: JSON 格式错误

let json = r#"{"name": "Alice",}"#;  // ❌ 多余的逗号
let v: Value = serde_json::from_str(json).unwrap();

错误信息:

trailing comma at line 1 column 20

修复方法:

let json = r#"{"name": "Alice"}"#;  // ✅ 正确的 JSON

动手练习

练习 1: 解析用户数据

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    // TODO: 定义字段
    // - id (u32)
    // - username (String)
    // - email (String)
}

fn main() {
    let json = r#"{
        "id": 1,
        "username": "rustacean",
        "email": "rust@example.com"
    }"#;

    // TODO: 解析 JSON
    // TODO: 打印用户信息
}
点击查看答案
#[derive(Serialize, Deserialize, Debug)]
struct User {
    id: u32,
    username: String,
    email: String,
}

fn main() {
    let user: User = serde_json::from_str(json).unwrap();
    println!("用户:{:?}", user);
}

练习 2: 序列化配置

#[derive(Serialize)]
struct Config {
    // TODO: 定义配置字段
    // - host (String)
    // - port (u16)
    // - debug (bool)
}

fn main() {
    let config = Config {
        host: "localhost".to_string(),
        port: 8080,
        debug: true,
    };

    // TODO: 序列化为 JSON
    // TODO: 打印 JSON 字符串
}
点击查看答案
#[derive(Serialize)]
struct Config {
    host: String,
    port: u16,
    debug: bool,
}

fn main() {
    let config = Config {
        host: "localhost".to_string(),
        port: 8080,
        debug: true,
    };

    let json = serde_json::to_string(&config).unwrap();
    println!("{}", json);
}

练习 3: 处理嵌套结构

#[derive(Deserialize, Debug)]
struct Post {
    id: u32,
    title: String,
    author: Author,
}

#[derive(Deserialize, Debug)]
struct Author {
    name: String,
    email: String,
}

fn main() {
    let json = r#"{
        "id": 1,
        "title": "Rust JSON",
        "author": {
            "name": "张三",
            "email": "zhang@example.com"
        }
    }"#;

    // TODO: 解析嵌套 JSON
    // TODO: 打印作者邮箱
}
点击查看答案
let post: Post = serde_json::from_str(json).unwrap();
println!("作者邮箱:{}", post.author.email);

故障排查 (FAQ)

Q: serde_json 和 serde 有什么区别?

A:

  • serde: 序列化/反序列化框架 (trait 定义)
  • serde_json: JSON 格式的具体实现
use serde::{Serialize, Deserialize};  // 框架
use serde_json;  // JSON 实现

Q: 如何处理可选字段?

A: 使用 Option<T>:

#[derive(Deserialize)]
struct User {
    name: String,      // 必需
    age: Option<u8>,   // 可选
}

Q: 如何自定义字段名?

A: 使用 #[serde(rename)]:

#[derive(Deserialize)]
struct User {
    #[serde(rename = "user_name")]
    name: String,
}

知识扩展

从文件读取

use std::fs::File;
use std::io::BufReader;

let file = File::open("config.json")?;
let reader = BufReader::new(file);
let config: Config = serde_json::from_reader(reader)?;

性能优化

// 使用 serde_json::Value 进行快速解析
let value: Value = serde_json::from_str(json)?;

// 使用 serde_path_to_error 获取详细错误位置
use serde_path_to_error;

let result: Result<T, _> = serde_path_to_error::deserialize(deserializer);

动态 JSON 处理

// 处理未知结构的 JSON
fn process_value(value: &Value) {
    match value {
        Value::Null => println!("null"),
        Value::Bool(b) => println!("bool: {}", b),
        Value::Number(n) => println!("number: {}", n),
        Value::String(s) => println!("string: {}", s),
        Value::Array(arr) => {
            for item in arr {
                process_value(item);
            }
        }
        Value::Object(obj) => {
            for (key, value) in obj {
                println!("{}: ", key);
                process_value(value);
            }
        }
    }
}

小结

核心要点

  1. serde + serde_json: Rust 的 JSON 处理标准
  2. Value vs Struct: 无类型 vs 有类型解析
  3. derive 宏: 自动实现 Serialize/Deserialize
  4. 错误处理: 使用 Result 处理解析错误
  5. 自定义行为: 使用 serde 属性自定义序列化

关键术语

  • Serialization (序列化): Rust 结构体 → JSON
  • Deserialization (反序列化): JSON → Rust 结构体
  • Value: 无类型的 JSON 值
  • derive macro: 自动派生 trait 实现

术语表

English中文
Serialization序列化
Deserialization反序列化
Serialize序列化 trait
Deserialize反序列化 trait
derive macro派生宏
Value值类型

知识检查

快速测验(答案在下方):

  1. serde_json::Value 和类型化反序列化有什么区别?

  2. 如何处理 JSON 中可能缺失的字段?

  3. #[serde(flatten)] 的作用是什么?

点击查看答案与解析
  1. Value 是无类型的动态 JSON 对象,类型化反序列化直接得到结构体
  2. 使用 Option<T> 字段,或 #[serde(default)]
  3. 将嵌套对象"展平"到当前结构体中

关键理解: 优先使用类型化反序列化,Value 仅在动态场景使用。

继续学习

前一章: Hyper HTTP 库
下一章: CSV 处理

相关章节:

返回: 高级进阶


完整示例: json_sample.rs

CSV 文件处理

开篇故事

想象你收到一份 Excel 表格,里面是员工信息:ID、姓名、年龄、部门、薪资。你需要筛选出薪资大于 5000 且年龄小于 50 的员工,然后保存到新文件。手动操作很繁琐,而 Rust 的 CSV 库可以自动完成这个任务。


本章适合谁

如果你需要在 Rust 程序中处理 CSV 文件(读取数据、分析数据、导出数据),本章适合你。CSV 是最常用的数据交换格式之一。


你会学到什么

完成本章后,你可以:

  1. 读取和解析 CSV 文件
  2. 将 CSV 数据映射到结构体
  3. 写入 CSV 数据
  4. 处理 CSV 错误和验证
  5. 处理大型 CSV 文件

前置要求

  • 结构体 - 结构体定义
  • Serde 序列化 - 序列化基础
  • 错误处理 - 错误处理基础

依赖安装

运行以下命令安装所需依赖:

cargo add serde --features derive
cargo add csv

第一个例子

最简单的 CSV 读取:

use csv::Reader;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Record {
    city: String,
    region: String,
    country: String,
    population: Option<u64>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut reader = Reader::from_path("cities.csv")?;
    
    for result in reader.deserialize() {
        let record: Record = result?;
        println!("{:?}", record);
    }
    
    Ok(())
}

完整示例: csv_sample.rs


原理解析

CSV 特性

csv crate 是一个 CSV 处理库

  • ✅ 自动解析 CSV
  • ✅ Serde 集成
  • ✅ 处理各种 CSV 格式
  • ✅ 高性能

定义结构体

使用 serde 属性

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
struct Employee {
    #[serde(rename = "ID")]
    id: u32,
    #[serde(rename = "Name")]
    name: String,
    #[serde(rename = "Age")]
    age: u8,
    #[serde(rename = "Department")]
    department: String,
    #[serde(rename = "Salary")]
    salary: f64,
}

serde 属性

  • rename: 指定 CSV 列名映射
  • default: 提供默认值
  • skip: 跳过某列

读取 CSV

从文件读取

use csv::Reader;

let mut reader = Reader::from_path("data.csv")?;

for result in reader.deserialize() {
    let record: Record = result?;
    println!("{:?}", record);
}

从字符串读取

use csv::ReaderBuilder;

let data = "name,age\nAlice,30\nBob,25";
let mut reader = ReaderBuilder::new()
    .has_headers(true)
    .from_reader(data.as_bytes());

for result in reader.deserialize() {
    let record: Record = result?;
    println!("{:?}", record);
}

自定义解析

指定分隔符

let mut reader = ReaderBuilder::new()
    .delimiter(b';')  // 使用分号分隔
    .has_headers(true)
    .from_path("data.csv")?;

无表头

let mut reader = ReaderBuilder::new()
    .has_headers(false)
    .from_path("data.csv")?;

写入 CSV

写入文件

use csv::WriterBuilder;

let mut writer = WriterBuilder::new()
    .delimiter(b',')
    .has_headers(false)
    .from_path("output.csv")?;

// 写入表头
writer.write_record(&["ID", "Name", "Age", "Department", "Salary"])?;

// 写入数据
for employee in employees {
    writer.serialize(employee)?;
}

writer.flush()?;  // 确保数据写入磁盘

过滤和转换

过滤数据

// 过滤:薪资 > 5000 且年龄 < 50
let filtered: Vec<_> = employees
    .into_iter()
    .filter(|e| e.salary > 5000.0 && e.age < 50)
    .collect();

转换数据

// 转换:添加新字段
let transformed: Vec<_> = employees
    .into_iter()
    .map(|e| {
        (e.name, e.salary * 1.1)  // 加薪 10%
    })
    .collect();

处理大型文件

逐行处理

use csv::Reader;

let mut reader = Reader::from_path("large_data.csv")?;

// 逐行读取并处理
for result in reader.deserialize::<Employee>() {
    let employee: Employee = result?;
    // 逐条处理避免内存溢出
    println!("{}", employee.name);
}

常见错误

错误 1: 列名不匹配

#[derive(Deserialize)]
struct Employee {
    id: u32,
    name: String,
    age: u8,
}

// CSV 文件列名:ID,Name,Age
// ❌ 小写不匹配

错误信息:

CSV deserialize error: field 'id' not found

修复方法:

#[derive(Deserialize)]
struct Employee {
    #[serde(rename = "ID")]
    id: u32,
    #[serde(rename = "Name")]
    name: String,
    #[serde(rename = "Age")]
    age: u8,
}

错误 2: 类型不匹配

#[derive(Deserialize)]
struct Employee {
    age: u8,
}

// CSV 数据:age
//           twenty
// ❌ 期望数字,得到字符串

错误信息:

CSV deserialize error: field 'age': invalid type: string "twenty", expected u8

修复方法: 确保 CSV 数据类型正确,或使用 Option 处理空值。

错误 3: 文件不存在

let mut reader = Reader::from_path("nonexistent.csv")?;
// ❌ 文件不存在

错误信息:

No such file or directory (os error 2)

修复方法:

use std::path::Path;

let path = "data.csv";
if !Path::new(path).exists() {
    eprintln!("文件不存在:{}", path);
    return;
}

let mut reader = Reader::from_path(path)?;

动手练习

练习 1: 定义员工结构体

use serde::{Deserialize, Serialize};

// TODO: 定义 Employee 结构体
// 字段:id (u32), name (String), department (String), salary (f64)
// 使用 serde 属性映射 CSV 列名
点击查看答案
#[derive(Debug, Deserialize, Serialize)]
struct Employee {
    #[serde(rename = "ID")]
    id: u32,
    #[serde(rename = "Name")]
    name: String,
    #[serde(rename = "Department")]
    department: String,
    #[serde(rename = "Salary")]
    salary: f64,
}

练习 2: 读取和打印员工

use csv::Reader;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut reader = Reader::from_path("employees.csv")?;
    
    // TODO: 遍历所有员工
    // TODO: 打印每个员工信息
}
点击查看答案
for result in reader.deserialize() {
    let employee: Employee = result?;
    println!("{} - {} - {}", employee.name, employee.department, employee.salary);
}

练习 3: 过滤和写入

// TODO: 读取员工数据
// TODO: 过滤薪资 > 5000 的员工
// TODO: 写入到新 CSV 文件
点击查看答案
let mut reader = Reader::from_path("employees.csv")?;
let employees: Vec<Employee> = reader.deserialize().collect()?;

let filtered: Vec<_> = employees
    .into_iter()
    .filter(|e| e.salary > 5000.0)
    .collect();

let mut writer = Writer::from_path("high_salary.csv")?;
for employee in filtered {
    writer.serialize(employee)?;
}
writer.flush()?;

故障排查 (FAQ)

Q: 如何处理空值?

A: 使用 Option<T>:

#[derive(Deserialize)]
struct Record {
    name: String,
    age: Option<u8>,  // 可以是空值
}

Q: 如何处理不同的分隔符?

A:

let mut reader = ReaderBuilder::new()
    .delimiter(b';')  // 分号
    .from_path("data.csv")?;

Q: 如何处理大文件?

A: 逐行处理,不要一次性加载:

for result in reader.deserialize() {
    let record: Record = result?;
    // 逐条处理
}

知识扩展

自定义解析器

use csv::{ReaderBuilder, StringRecord};

let mut reader = ReaderBuilder::new()
    .has_headers(false)
    .from_path("data.csv")?;

for result in reader.records() {
    let record = result?;
    println!("Column 1: {}", record.get(0).unwrap());
    println!("Column 2: {}", record.get(1).unwrap());
}

并行处理

use rayon::prelude::*;

let employees: Vec<Employee> = reader
    .deserialize()
    .par_bridge()  // 并行处理
    .collect::<Result<_, _>>()?;

性能优化

// 预分配容量
let mut employees = Vec::with_capacity(1000);

for result in reader.deserialize() {
    employees.push(result?);
}

小结

核心要点

  1. csv crate: CSV 处理标准库
  2. Serde 集成: 自动映射到结构体
  3. Reader/Writer: 读写 CSV
  4. 错误处理: 使用 Result 处理解析错误
  5. 大文件处理: 逐行处理避免内存溢出

关键术语

  • CSV: 逗号分隔值
  • Deserialize: 反序列化
  • Serialize: 序列化
  • Reader: CSV 读取器
  • Writer: CSV 写入器

术语表

English中文
CSV逗号分隔值
Deserialize反序列化
Serialize序列化
Reader读取器
Writer写入器
Delimiter分隔符

知识检查

快速测验(答案在下方):

  1. CSV 和 JSON 序列化有什么区别?

  2. 如何处理 CSV 中不同类型的列?

  3. csv::Readercsv::StringRecord 的区别?

点击查看答案与解析
  1. CSV 是表格格式(行/列),JSON 是树形格式
  2. 使用 Serde 反序列化到结构体,自动类型转换
  3. Reader 是迭代器,StringRecord 是单行数据

关键理解: CSV 适合表格数据,JSON 适合嵌套数据。

继续学习

前一章: JSON 序列化
下一章: 零拷贝序列化

相关章节:

返回: 高级进阶


完整示例: csv_sample.rs

Rkyv 零拷贝序列化

开篇故事

想象你要寄一本很厚的书。传统方式是:复印整本书 → 打包 → 邮寄 → 收件人阅读。零拷贝就像是:直接把书递给收件人,不需要复印。Rkyv 就是这样的零拷贝序列化库,特别适合大数据集。


本章适合谁

如果你需要高性能序列化(处理大数据集、网络传输),本章适合你。Rkyv 是零拷贝序列化库,性能远超传统序列化方法。


你会学到什么

完成本章后,你可以:

  1. 理解零拷贝序列化概念
  2. 使用 rkyv 序列化和反序列化
  3. 使用归档类型 (Archived types)
  4. 自定义序列化配置
  5. 处理复杂数据结构

前置要求

  • 结构体 - 结构体定义
  • Serde 序列化 - 序列化基础
  • 枚举 - 枚举类型

依赖安装

运行以下命令安装所需依赖:

cargo add rkyv --features alloc

第一个例子

最简单的 Rkyv 序列化:

use rkyv::{Archive, Deserialize, Serialize};

#[derive(Archive, Deserialize, Serialize, Debug, PartialEq)]
struct TestStruct {
    int: u8,
    string: String,
    option: Option<Vec<i32>>,
}

fn main() {
    let value = TestStruct {
        int: 42,
        string: "hello world".to_string(),
        option: Some(vec![1, 2, 3, 4]),
    };

    // 序列化
    let bytes = rkyv::to_bytes::<_, 256>(&value).unwrap();
    
    // 零拷贝反序列化
    let archived = rkyv::access::<ArchivedTestStruct>(&bytes[..]).unwrap();
    
    // 使用归档数据(零拷贝)
    assert_eq!(archived.int, 42);
    assert_eq!(archived.string, "hello world");
    
    // 完整反序列化
    let deserialized = archived.deserialize(&mut rkyv::Infallible).unwrap();
    assert_eq!(deserialized, value);
}

完整示例: rkyv_sample.rs


原理解析

Rkyv 特性

Rkyv 是零拷贝序列化库

  • ✅ 零拷贝反序列化
  • ✅ 高性能
  • ✅ 支持归档类型
  • ✅ 无运行时开销

什么是零拷贝?

传统序列化

数据 → 序列化 → 字节数组 → 反序列化 → 新数据
       (复制)              (复制)

零拷贝序列化

数据 → 序列化 → 字节数组 → 直接访问 (无复制)
                      ↑
                  直接使用内存

派生宏

使用 Archive derive

use rkyv::{Archive, Deserialize, Serialize};

#[derive(Archive, Deserialize, Serialize, Debug)]
#[rkyv(derive(Debug, PartialEq))]
struct Person {
    name: String,
    age: u32,
}

生成的归档类型

  • PersonArchivedPerson
  • 可以直接访问字段,无需反序列化

序列化

简单序列化

let value = Person {
    name: "Alice".to_string(),
    age: 30,
};

// 序列化到字节数组
let bytes = rkyv::to_bytes::<_, 256>(&value).unwrap();

自定义序列化

use rkyv::ser::allocator::Arena;

let mut arena = Arena::new();
let bytes = rkyv::to_bytes_with_alloc::<_, 256>(&value, arena.acquire()).unwrap();

反序列化

零拷贝访问

let archived = rkyv::access::<ArchivedPerson>(&bytes[..]).unwrap();

// 直接访问字段(零拷贝)
println!("Name: {}", archived.name);
println!("Age: {}", archived.age);

完整反序列化

let deserialized = archived
    .deserialize(&mut rkyv::Infallible)
    .unwrap();

assert_eq!(deserialized.name, "Alice");

复杂类型

Vec 和 Option

#[derive(Archive, Deserialize, Serialize)]
struct Complex {
    numbers: Vec<i32>,
    maybe_string: Option<String>,
}

let value = Complex {
    numbers: vec![1, 2, 3],
    maybe_string: Some("hello".to_string()),
};

let bytes = rkyv::to_bytes::<_, 1024>(&value).unwrap();
let archived = rkyv::access::<ArchivedComplex>(&bytes[..]).unwrap();

// 零拷贝访问
for num in archived.numbers.iter() {
    println!("{}", num);
}

if let Some(s) = &archived.maybe_string {
    println!("{}", s);
}

常见错误

错误 1: 忘记 derive Archive

#[derive(Serialize, Deserialize)]  // ❌ 忘记 Archive
struct Person {
    name: String,
}

错误信息:

the trait `Archive` is not implemented for `Person`

修复方法:

#[derive(Archive, Serialize, Deserialize)]  // ✅ 添加 Archive
struct Person {
    name: String,
}

错误 2: 缓冲区太小

let bytes = rkyv::to_bytes::<_, 16>(&value).unwrap();
// ❌ 16 字节太小,无法容纳数据

错误信息:

Out of space

修复方法:

let bytes = rkyv::to_bytes::<_, 256>(&value).unwrap();  // ✅ 增加缓冲区

错误 3: 访问已释放内存

let bytes = serialize_data();
let archived = access::<ArchivedData>(&bytes);
drop(bytes);  // ❌ 释放内存

// 使用 archived 会导致未定义行为
println!("{}", archived.field);

修复方法: 确保字节数组的生命周期覆盖整个访问过程。


动手练习

练习 1: 定义简单结构体

use rkyv::{Archive, Deserialize, Serialize};

// TODO: 定义 Point 结构体
// 字段:x (i32), y (i32)
// 派生 Archive, Deserialize, Serialize
点击查看答案
#[derive(Archive, Deserialize, Serialize, Debug)]
#[rkyv(derive(Debug))]
struct Point {
    x: i32,
    y: i32,
}

练习 2: 序列化和反序列化

let point = Point { x: 10, y: 20 };

// TODO: 序列化为字节
// TODO: 零拷贝访问
// TODO: 完整反序列化
点击查看答案
let bytes = rkyv::to_bytes::<_, 64>(&point).unwrap();
let archived = rkyv::access::<ArchivedPoint>(&bytes[..]).unwrap();
assert_eq!(archived.x, 10);
assert_eq!(archived.y, 20);

let deserialized = archived.deserialize(&mut rkyv::Infallible).unwrap();
assert_eq!(deserialized.x, 10);

练习 3: 处理复杂类型

#[derive(Archive, Deserialize, Serialize)]
struct User {
    name: String,
    hobbies: Vec<String>,
}

// TODO: 创建 User 实例
// TODO: 序列化
// TODO: 零拷贝访问 hobbies
点击查看答案
let user = User {
    name: "Alice".to_string(),
    hobbies: vec!["Reading".to_string(), "Coding".to_string()],
};

let bytes = rkyv::to_bytes::<_, 256>(&user).unwrap();
let archived = rkyv::access::<ArchivedUser>(&bytes[..]).unwrap();

for hobby in archived.hobbies.iter() {
    println!("{}", hobby);
}

故障排查 (FAQ)

Q: Rkyv 和 Serde 有什么区别?

A:

  • Serde: 通用序列化框架,支持多种格式
  • Rkyv: 零拷贝序列化,性能最优,仅支持 rkyv 格式
  • 推荐: Rkyv(性能关键场景),Serde(通用场景)

Q: 如何选择合适的缓冲区大小?

A:

// 小数据:64-256 字节
rkyv::to_bytes::<_, 256>(&value).unwrap()

// 大数据:使用 Vec
let mut serializer = rkyv::Serializer::new();
rkyv::serialize_into(&mut serializer, &value).unwrap();
let bytes = serializer.into_serializer().into_inner();

Q: 如何处理循环引用?

A: Rkyv 不支持循环引用。使用 RcArc 重构数据结构。


知识扩展

自定义分配器

use rkyv::ser::allocator::Arena;

let mut arena = Arena::new();
let bytes = rkyv::to_bytes_with_alloc::<_, 256>(&value, arena.acquire()).unwrap();

验证归档

use rkyv::validation::validators::DefaultValidator;

let archived = rkyv::access::<ArchivedData>(&bytes[..]).unwrap();
archived.validate(&mut DefaultValidator::default()).unwrap();

性能对比

// Serde
let bytes = bincode::serialize(&value).unwrap();
let deserialized: Data = bincode::deserialize(&bytes).unwrap();

// Rkyv (更快)
let bytes = rkyv::to_bytes::<_, 256>(&value).unwrap();
let archived = rkyv::access::<ArchivedData>(&bytes[..]).unwrap();

小结

核心要点

  1. Rkyv: 零拷贝序列化库
  2. Archive: 归档类型,直接访问内存
  3. 零拷贝: 无需反序列化即可访问数据
  4. 高性能: 远超传统序列化方法
  5. 缓冲区: 需要足够大小的缓冲区

关键术语

  • Zero-copy (零拷贝): 无需复制即可访问数据
  • Archive (归档): 序列化后的内存布局
  • Archived type (归档类型): 自动生成的归档类型
  • Serializer (序列化器): 序列化数据的工具

术语表

English中文
Zero-copy零拷贝
Archive归档
Archived type归档类型
Serializer序列化器
Deserializer反序列化器
Buffer缓冲区

知识检查

快速测验(答案在下方):

  1. rkyv 和 serde 的主要区别是什么?

  2. 什么是"零拷贝"反序列化?

  3. 什么时候应该使用 rkyv 而不是 serde?

点击查看答案与解析
  1. rkyv 是零拷贝序列化,serde 需要反序列化到内存
  2. 直接访问序列化后的内存,无需复制到新结构
  3. 性能关键场景:游戏、数据库、网络传输大数据

关键理解: rkyv 牺牲兼容性换取极致性能。

继续学习

前一章: CSV 处理
下一章: 临时文件

相关章节:

返回: 高级进阶


完整示例: rkyv_sample.rs

文件与目录操作

开篇故事

想象你在整理一个巨大的仓库。传统方式是:走进仓库 → 找到物品 → 拿出来 → 走回办公室记录。每次只能处理一件物品,效率极低。

更聪明的做法是:使用智能仓库管理系统——你可以一次性列出所有物品、批量移动、按类别搜索,甚至在不同房间之间建立快捷通道。Rust 的文件与目录操作就是你的"智能仓库管理系统"——它让你高效地管理文件系统。


本章适合谁

如果你需要在 Rust 程序中读写文件、遍历目录、处理路径,本章适合你。文件系统操作是几乎所有应用程序的基础需求。


你会学到什么

完成本章后,你可以:

  1. 使用 std::fs 读写文件
  2. 使用 std::path::PathPathBuf 处理路径
  3. 遍历目录树
  4. 创建和删除文件/目录
  5. 获取文件和目录元数据
  6. 处理跨平台路径差异

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add home
cargo add dotenvy

第一个例子

读取文件内容:

use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 读取整个文件到字符串
    let content = fs::read_to_string("hello.txt")?;
    println!("文件内容:\n{}", content);
    Ok(())
}

发生了什么?

  • fs::read_to_string - 读取文件并返回 String
  • ? - 错误传播(文件不存在或无权限时返回错误)

原理解析

1. 文件系统树形结构

/ (根目录)
├── home/
│   └── user/
│       ├── documents/
│       │   ├── report.txt
│       │   └── notes.md
│       └── pictures/
│           └── photo.jpg
├── etc/
│   └── config.ini
└── tmp/
    └── temp_file.txt

2. 路径处理

use std::path::{Path, PathBuf};

// Path - 借用路径(不拥有所有权)
let path = Path::new("hello.txt");
println!("文件名:{}", path.file_name().unwrap().to_str().unwrap());

// PathBuf - 拥有路径(可修改)
let mut path_buf = PathBuf::from("/home");
path_buf.push("user");
path_buf.push("documents");
println!("完整路径:{}", path_buf.display());

// 推荐:使用 join 构建路径
let path = Path::new("/home")
    .join("user")
    .join("documents")
    .join("report.txt");

3. 文件读写操作

use std::fs::{self, File};
use std::io::{Read, Write};

// 读取整个文件
let content = fs::read_to_string("file.txt")?;

// 读取为字节
let bytes = fs::read("file.txt")?;

// 写入整个文件(覆盖)
fs::write("output.txt", "Hello, World!")?;

// 追加写入
let mut file = File::options()
    .create(true)
    .append(true)
    .open("log.txt")?;
file.write_all(b"New log entry\n")?;

4. 目录遍历

use std::fs;

// 读取目录内容
let entries = fs::read_dir("/home/user")?;
for entry in entries {
    let entry = entry?;
    println!("文件名:{}", entry.file_name().to_str().unwrap());
    println!("路径:{}", entry.path().display());
    
    let metadata = entry.metadata()?;
    println!("是文件:{}", metadata.is_file());
    println!("是目录:{}", metadata.is_dir());
}

5. 递归目录遍历

use std::fs;
use std::path::Path;

fn list_dir_recursive(path: &Path, prefix: &str) -> std::io::Result<()> {
    let entries = fs::read_dir(path)?;
    
    for entry in entries {
        let entry = entry?;
        let path = entry.path();
        let name = entry.file_name();
        
        println!("{}{}", prefix, name.to_str().unwrap());
        
        if path.is_dir() {
            list_dir_recursive(&path, &format!("{}  ", prefix))?;
        }
    }
    Ok(())
}

fn main() -> std::io::Result<()> {
    list_dir_recursive(Path::new("."), "")
}

6. 文件和目录操作

use std::fs;

// 创建目录
fs::create_dir("new_dir")?;
fs::create_dir_all("parent/child/grandchild")?;  // 递归创建

// 删除文件
fs::remove_file("file.txt")?;

// 删除空目录
fs::remove_dir("empty_dir")?;

// 删除目录及其内容
fs::remove_dir_all("dir_with_contents")?;

// 复制文件
fs::copy("source.txt", "dest.txt")?;

// 重命名/移动
fs::rename("old_name.txt", "new_name.txt")?;

7. 文件元数据

use std::fs;

let metadata = fs::metadata("file.txt")?;

println!("文件大小:{} 字节", metadata.len());
println!("是文件:{}", metadata.is_file());
println!("是目录:{}", metadata.is_dir());

// 权限(Unix 系统)
#[cfg(unix)]
{
    use std::os::unix::fs::PermissionsExt;
    let perms = metadata.permissions();
    println!("权限:{:o}", perms.mode());
}

常见错误

错误 1: 路径拼接使用字符串

// ❌ 错误:跨平台不兼容
let path = format!("{}/{}", dir, filename);

// ✅ 正确:使用 Path::join
let path = Path::new(dir).join(filename);

错误 2: 不处理文件不存在

// ❌ 错误:panic 如果文件不存在
let content = fs::read_to_string("missing.txt").unwrap();

// ✅ 正确:处理错误
match fs::read_to_string("missing.txt") {
    Ok(content) => println!("{}", content),
    Err(e) => eprintln!("无法读取文件:{}", e),
}

动手练习

练习 1: 统计目录中文件类型

编写程序统计目录中各种文件类型的数量:

// TODO: 实现 count_file_types 函数
// 接受一个目录路径
// 返回 HashMap<扩展名,数量>
点击查看答案
use std::collections::HashMap;
use std::fs;
use std::path::Path;

fn count_file_types(path: &Path) -> std::io::Result<HashMap<String, usize>> {
    let mut counts = HashMap::new();
    
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let path = entry.path();
        
        if path.is_file() {
            let ext = path
                .extension()
                .and_then(|e| e.to_str())
                .unwrap_or("no_extension")
                .to_string();
            
            *counts.entry(ext).or_insert(0) += 1;
        }
    }
    
    Ok(counts)
}

故障排查

Q: 如何获取 HOME 目录?

A:

// 方法 1: std::env
std::env::home_dir()

// 方法 2: home crate (推荐)
home::home_dir()

Q: 如何处理大文件?

A: 使用 BufReaderBufWriter 逐行/逐块处理:

use std::io::{BufReader, BufRead};
let file = File::open("large.txt")?;
let reader = BufReader::new(file);
for line in reader.lines() {
    // 处理每一行
}

Q: 跨平台路径分隔符?

A: 永远使用 Path::joinPathBuf::push,不要硬编码 /\


小结

核心要点

  1. std::fs: 文件和目录操作
  2. Path/PathBuf: 路径处理
  3. read_dir: 目录遍历
  4. metadata: 文件信息

术语表

English中文
File System文件系统
Path路径
Directory目录
Metadata元数据
Recursion递归
Cross-platform跨平台

完整示例:src/advance/system/directory_sample.rs


知识检查

快速测验(答案在下方):

  1. PathPathBuf 有什么区别?

  2. 为什么不应该使用字符串拼接路径?

  3. fs::read_dir 返回什么类型?

点击查看答案与解析
  1. Path 是借用类型,PathBuf 是拥有类型
  2. 跨平台不兼容(Windows 用 \,Unix 用 /
  3. Result<ReadDir, io::Error>,迭代返回 Result<DirEntry, io::Error>

关键理解: 始终使用 Path::join 构建路径。

继续学习

💡 记住:始终使用 Path/PathBuf 处理路径,确保跨平台兼容!

临时文件处理

开篇故事

想象你在写一份草稿,需要临时保存一下。你不会把它放进永久文件夹,而是放在桌面的临时区域,用完就清理。tempfile 库就像这个临时区域——帮你创建临时文件和目录,用完自动清理。


本章适合谁

如果你需要在 Rust 程序中创建临时文件(缓存数据、中间结果、测试数据),本章适合你。临时文件是处理大量数据或测试的常用技术。


你会学到什么

完成本章后,你可以:

  1. 创建临时文件
  2. 创建临时目录
  3. 读写临时文件
  4. 自动清理临时资源
  5. 处理临时文件错误

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add tempfile

第一个例子

最简单的临时文件创建:

use std::io::{Write, Read, Seek, SeekFrom};
use tempfile::tempfile;

fn main() -> std::io::Result<()> {
    // 创建临时文件
    let mut tmpfile = tempfile()?;
    
    // 写入数据
    write!(tmpfile, "Hello World!")?;
    
    // 重置文件指针
    tmpfile.seek(SeekFrom::Start(0))?;
    
    // 读取数据
    let mut buf = String::new();
    tmpfile.read_to_string(&mut buf)?;
    
    assert_eq!("Hello World!", buf);
    
    // 临时文件在离开作用域时自动删除
    Ok(())
}

完整示例: tempfile_sample.rs


原理解析

tempfile 特性

tempfile 是一个临时文件处理库

  • ✅ 自动清理临时文件
  • ✅ 跨平台支持
  • ✅ 安全创建(防竞争条件)
  • ✅ 临时目录支持

创建临时文件

使用 tempfile()

use tempfile::tempfile;

let mut file = tempfile()?;
// 使用临时文件
// 离开作用域时自动删除

使用 NamedTempFile

use tempfile::NamedTempFile;

let file = NamedTempFile::new()?;
// 可以获取文件路径
let path = file.path();
println!("临时文件路径:{}", path.display());

创建临时目录

使用 tempdir()

use tempfile::tempdir;

let dir = tempdir()?;
let file_path = dir.path().join("test.txt");

// 在临时目录中创建文件
std::fs::write(&file_path, "Hello!")?;

// 目录和文件在离开作用域时自动删除
drop(dir);

在指定目录创建

使用 new_in()

use tempfile::NamedTempFile;
use std::env;

let home_dir = env::home_dir().unwrap();
let file = NamedTempFile::new_in(&home_dir)?;

println!("在 home 目录创建:{:?}", file);

重新打开临时文件

使用 reopen()

use tempfile::NamedTempFile;
use std::io::{Write, Read};

let text = "Hello World!";

let mut file1 = NamedTempFile::new()?;

// 写入数据
file1.write_all(text.as_bytes())?;

// 重新打开(获得新的文件句柄)
let mut file2 = file1.reopen()?;

// 从第二个句柄读取
let mut buf = String::new();
file2.read_to_string(&mut buf)?;

assert_eq!(buf, text);

显式清理

使用 close()

use tempfile::tempdir;

let dir = tempdir()?;

// ... 使用临时目录 ...

// 显式清理并检查是否成功
dir.close().unwrap();

常见错误

错误 1: 临时文件权限问题

let file = NamedTempFile::new_in("/root")?;
// ❌ 如果没有权限,会失败

错误信息:

Permission denied (os error 13)

修复方法:

// 使用有权限的目录
let file = NamedTempFile::new_in("/tmp")?;

错误 2: 忘记处理错误

let file = NamedTempFile::new();  // ❌ 返回 Result,需要处理

错误信息:

unused `Result` that must be used

修复方法:

let file = NamedTempFile::new()?;  // ✅ 使用 ? 处理错误

错误 3: 临时文件过早删除

fn create_temp() -> std::path::PathBuf {
    let file = NamedTempFile::new().unwrap();
    file.path().to_path_buf()  // ❌ 文件已被删除
}

修复方法:

fn keep_temp() -> tempfile::NamedTempFile {
    let file = NamedTempFile::new().unwrap();
    file  // ✅ 返回文件,保持存活
}

动手练习

练习 1: 创建临时文件

use tempfile::tempfile;
use std::io::{Write, Read};

fn main() -> std::io::Result<()> {
    // TODO: 创建临时文件
    // TODO: 写入 "Hello Tempfile!"
    // TODO: 读取并打印内容
}
点击查看答案
let mut tmpfile = tempfile()?;
write!(tmpfile, "Hello Tempfile!")?;

tmpfile.seek(SeekFrom::Start(0))?;

let mut buf = String::new();
tmpfile.read_to_string(&mut buf)?;

println!("{}", buf);

练习 2: 创建临时目录

use tempfile::tempdir;
use std::fs;

fn main() -> std::io::Result<()> {
    // TODO: 创建临时目录
    // TODO: 在目录中创建文件
    // TODO: 写入内容并读取
}
点击查看答案
let dir = tempdir()?;
let file_path = dir.path().join("test.txt");

fs::write(&file_path, "Hello from tempdir!")?;

let content = fs::read_to_string(&file_path)?;
println!("{}", content);

dir.close().unwrap();

练习 3: 重新打开临时文件

use tempfile::NamedTempFile;
use std::io::{Write, Read};

fn main() -> std::io::Result<()> {
    let text = "Test data";
    
    // TODO: 创建命名临时文件
    // TODO: 写入数据
    // TODO: 重新打开并读取
    
    let mut file1 = NamedTempFile::new()?;
    // ... 完成练习
}
点击查看答案
let mut file1 = NamedTempFile::new()?;
file1.write_all(text.as_bytes())?;

let mut file2 = file1.reopen()?;

let mut buf = String::new();
file2.read_to_string(&mut buf)?;

assert_eq!(buf, text);

故障排查 (FAQ)

Q: 临时文件什么时候被删除?

A:

  • tempfile(): 文件句柄关闭时自动删除
  • NamedTempFile: 离开作用域或调用 close() 时删除
  • tempdir: 离开作用域或调用 close() 时删除

Q: 如何保持临时文件不删除?

A:

use tempfile::NamedTempFile;
use std::fs::File;

let temp = NamedTempFile::new()?;

// 转换为永久文件
let path = temp.path().to_path_buf();
temp.persist(&path)?;  // 不删除

Q: 如何指定临时文件扩展名?

A:

use tempfile::NamedTempFile;

let file = NamedTempFile::new()
    .unwrap()
    .keep()  // 保持文件
    .1;  // 获取路径

知识扩展

临时文件与进程

use tempfile::tempfile;

// 临时文件对当前进程可见
let file1 = tempfile()?;
let file2 = tempfile()?;

// 每个临时文件都是独立的

临时目录嵌套

use tempfile::tempdir;

let parent = tempdir()?;
let child_path = parent.path().join("child");

std::fs::create_dir(&child_path)?;

// 删除父目录时,子目录也会被删除
drop(parent);

性能优化

use tempfile::tempfile_in;
use std::env;

// 在 RAM disk 创建(如果可用)
let tmp_dir = env::var("TMPDIR").unwrap_or_else(|_| "/tmp".to_string());
let file = tempfile_in(&tmp_dir)?;

小结

核心要点

  1. tempfile: 自动清理临时文件
  2. NamedTempFile: 可获取路径的临时文件
  3. tempdir: 临时目录支持
  4. 自动清理: 离开作用域自动删除
  5. reopen(): 重新打开临时文件

关键术语

  • Temporary File (临时文件): 临时存储数据的文件
  • NamedTempFile: 命名临时文件
  • TempDir: 临时目录
  • Auto-cleanup: 自动清理

术语表

English中文
Temporary File临时文件
NamedTempFile命名临时文件
TempDir临时目录
Auto-cleanup自动清理
Scope作用域

知识检查

快速测验(答案在下方):

  1. tempfile()NamedTempFile 有什么区别?

  2. 临时文件什么时候被删除?

  3. TempDir 的作用是什么?

点击查看答案与解析
  1. tempfile() 无名称(自动删除),NamedTempFile 有路径(可持久化)
  2. TempFile 在 drop 时自动删除,NamedTempFile 可调用 persist() 保留
  3. TempDir 创建临时目录,drop 时删除目录及其内容

关键理解: 临时文件是测试和临时数据存储的理想选择。

继续学习

前一章: 内存映射
下一章: 环境变量

相关章节:

返回: 高级进阶


完整示例: tempfile_sample.rs

内存映射文件

开篇故事

想象你要读一本很厚的书。传统方式是:一页一页读 → 记住内容 → 合上书。内存映射就像是:把整本书摊开在桌子上,你可以直接翻阅任何一页,不需要逐页读取。memmap 就是这样的技术,特别适合大文件处理。


本章适合谁

如果你需要高效处理大文件(数据库文件、日志文件、大数据集),本章适合你。内存映射是高性能文件处理的关键技术。


你会学到什么

完成本章后,你可以:

  1. 理解内存映射概念
  2. 将文件映射到内存
  3. 直接访问文件内容
  4. 修改内存映射内容
  5. 获取系统页面大小

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add memmap2
cargo add tempfile

第一个例子

最简单的内存映射:

use std::io::{Write, Seek, SeekFrom};
use std::fs::File;
use memmap2::Mmap;
use tempfile::tempfile;

fn main() -> std::io::Result<()> {
    // 创建临时文件
    let mut file = tempfile()?;
    
    // 写入数据
    write!(file, "Hello World!")?;
    
    // 重置文件指针
    file.seek(SeekFrom::Start(0))?;
    
    // 映射文件到内存
    let mmap = unsafe { Mmap::map(&file)? };
    
    // 直接访问内存中的数据
    assert_eq!(b"Hello World!", &mmap[..]);
    
    println!("映射内容:{}", String::from_utf8_lossy(&mmap));
    
    Ok(())
}

完整示例: memmap_sample.rs


原理解析

什么是内存映射?

传统文件读取

文件 → 系统调用 → 内核缓冲区 → 用户缓冲区 → 程序
      (慢)          (复制)        (复制)

内存映射

文件 → 内存映射 → 程序直接访问
      (映射一次,后续直接访问)

memmap2 特性

memmap2 是内存映射库

  • ✅ 零拷贝读取
  • ✅ 高性能
  • ✅ 支持读写映射
  • ✅ 跨平台支持

只读映射

使用 Mmap::map()

use memmap2::Mmap;
use std::fs::File;

let file = File::open("data.txt")?;
let mmap = unsafe { Mmap::map(&file)? };

// 直接访问
println!("内容:{}", String::from_utf8_lossy(&mmap));

可写映射

使用 MmapMut

use memmap2::MmapMut;
use std::fs::File;

let file = File::create("data.txt")?;
file.set_len(4096)?;  // 设置文件大小

let mut mmap = unsafe { MmapMut::map_mut(&file)? };

// 写入数据
mmap[..12].copy_from_slice(b"Hello World!");

// 刷新到磁盘
mmap.flush()?;

使映射可写

使用 make_mut()

use memmap2::Mmap;

let mmap = unsafe { Mmap::map(&file)? };

// 使映射可写(会创建私有副本)
let mut mmap_mut = mmap.make_mut()?;

// 修改内容
mmap_mut[..5].copy_from_slice(b"Hello");

获取系统页面大小

使用 page_size::get()

use page_size;

let page_size = page_size::get();
println!("系统页面大小:{} bytes", page_size);

// 通常是 4096 bytes (4KB)

性能优势

大文件处理

use memmap2::Mmap;
use std::fs::File;

// 打开大文件(例如 1GB)
let file = File::open("large_data.bin")?;

// 内存映射(几乎瞬间完成)
let mmap = unsafe { Mmap::map(&file)? };

// 随机访问任意位置(无需读取整个文件)
let offset = 1024 * 1024;  // 1MB 处
println!("数据:{:?}", &mmap[offset..offset+100]);

常见错误

错误 1: 忘记设置文件大小

use memmap2::MmapMut;
use std::fs::File;

let file = File::create("data.txt")?;
let mmap = unsafe { MmapMut::map_mut(&file)? };
// ❌ 文件为空,无法映射

错误信息:

Invalid argument (os error 22)

修复方法:

file.set_len(4096)?;  // ✅ 先设置文件大小
let mmap = unsafe { MmapMut::map_mut(&file)? };

错误 2: 忘记使用 unsafe

let mmap = Mmap::map(&file)?;  // ❌ 缺少 unsafe

错误信息:

unsafe fn `map` requires unsafe function or block

修复方法:

let mmap = unsafe { Mmap::map(&file)? };  // ✅ 使用 unsafe 块

错误 3: 访问已释放的映射

fn create_mmap() -> Mmap {
    let file = File::open("data.txt").unwrap();
    unsafe { Mmap::map(&file).unwrap() }  // ❌ file 已被释放
}

修复方法:

fn keep_mmap() -> std::io::Result<Mmap> {
    let file = File::open("data.txt")?;
    let mmap = unsafe { Mmap::map(&file)? };
    Ok(mmap)  // ✅ file 和 mmap 一起返回
}

动手练习

练习 1: 创建只读映射

use memmap2::Mmap;
use std::fs::File;

fn main() -> std::io::Result<()> {
    // TODO: 打开文件
    // TODO: 创建内存映射
    // TODO: 打印前 100 字节
}
点击查看答案
let file = File::open("data.txt")?;
let mmap = unsafe { Mmap::map(&file)? };

println!("前 100 字节:{:?}", &mmap[..100.min(mmap.len())]);

练习 2: 创建可写映射

use memmap2::MmapMut;
use std::fs::File;

fn main() -> std::io::Result<()> {
    let file = File::create("test.bin")?;
    
    // TODO: 设置文件大小为 4096
    // TODO: 创建可写映射
    // TODO: 写入 "Hello Memory Map!"
    // TODO: 刷新到磁盘
}
点击查看答案
file.set_len(4096)?;

let mut mmap = unsafe { MmapMut::map_mut(&file)? };
mmap[..17].copy_from_slice(b"Hello Memory Map!");
mmap.flush()?;

练习 3: 获取系统页面大小

use page_size;

fn main() {
    // TODO: 获取系统页面大小
    // TODO: 打印结果
}
点击查看答案
let page_size = page_size::get();
println!("系统页面大小:{} bytes", page_size);

故障排查 (FAQ)

Q: 内存映射和普通读取有什么区别?

A:

  • 普通读取: 需要系统调用,数据复制到用户空间
  • 内存映射: 一次映射,后续直接访问内存
  • 性能: 内存映射在大文件场景性能更优

Q: 内存映射安全吗?

A:

  • 只读映射: 安全,不会修改原文件
  • 可写映射: 需要 unsafe,修改会反映到文件
  • 注意: 确保文件在映射期间不被删除

Q: 如何处理超大文件?

A:

use memmap2::Mmap;

// 映射整个文件(可能很大)
let mmap = unsafe { Mmap::map(&file)? };

// 只访问需要的部分
let chunk = &mmap[offset..offset+size];

知识扩展

异步内存映射

use tokio::fs::File;
use memmap2::Mmap;

// 注意:memmap2 是同步的
// 在 tokio 中使用 spawn_blocking
let mmap = tokio::task::spawn_blocking(move || {
    unsafe { Mmap::map(&file) }
}).await??;

匿名映射

use memmap2::MmapMut;

// 创建不关联文件的内存映射
let mmap = unsafe { MmapMut::map_anon(4096)? };

性能对比

// 传统读取
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;

// 内存映射(更快)
let mmap = unsafe { Mmap::map(&file)? };

小结

核心要点

  1. 内存映射: 将文件映射到内存地址空间
  2. Mmap: 只读内存映射
  3. MmapMut: 可写内存映射
  4. 零拷贝: 直接访问文件内容
  5. 性能优势: 大文件处理更高效

关键术语

  • Memory Map (内存映射): 文件到内存的映射
  • Mmap: 只读映射类型
  • MmapMut: 可写映射类型
  • Zero-copy (零拷贝): 无需数据复制

术语表

English中文
Memory Map内存映射
Mmap内存映射(只读)
MmapMut内存映射(可写)
Zero-copy零拷贝
Page Size页面大小

知识检查

快速测验(答案在下方):

  1. 内存映射和普通文件读写有什么区别?

  2. 什么时候应该使用内存映射?

  3. MmapMutMmap 的区别?

点击查看答案与解析
  1. 内存映射将文件映射到内存,直接访问无需系统调用
  2. 大文件随机访问、进程间共享内存
  3. MmapMut 可写,Mmap 只读

关键理解: 内存映射适合大文件,小文件用普通 I/O 更简单。

继续学习

前一章: 临时文件
下一章: 环境变量

相关章节:

返回: 高级进阶


完整示例: memmap_sample.rs

环境变量配置

开篇故事

想象你要配置一个应用程序。硬编码配置(如数据库密码)就像把密码写在代码里——不安全且难以修改。环境变量就像配置文件——可以随时修改而不需要重新编译。.env 文件就是这样的配置文件。


本章适合谁

如果你需要在 Rust 程序中管理配置(数据库连接、API 密钥、环境设置),本章适合你。环境变量是管理配置的标准方法。


你会学到什么

完成本章后,你可以:

  1. 从 .env 文件加载环境变量
  2. 读取环境变量
  3. 获取系统目录路径
  4. 构建相对路径
  5. 处理环境变量错误

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add dotenvy
cargo add home

第一个例子

最简单的 .env 加载:

use dotenvy;
use std::env;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 从 .env 文件加载环境变量
    dotenvy::dotenv()?;
    
    // 读取环境变量
    let database_url = env::var("DATABASE_URL")?;
    println!("数据库:{}", database_url);
    
    // 遍历所有环境变量
    for (key, value) in env::vars() {
        println!("{}: {}", key, value);
    }
    
    Ok(())
}

完整示例: dotenv_sample.rs


原理解析

dotenvy 特性

dotenvy 是环境变量加载库

  • ✅ 从 .env 文件加载
  • ✅ 覆盖现有环境变量
  • ✅ 错误处理
  • ✅ 跨平台支持

.env 文件格式

标准格式

# 注释
DATABASE_URL=postgres://localhost/mydb
API_KEY=your_api_key_here
DEBUG=true
PORT=3000

带空格的值

MESSAGE="Hello World"

加载环境变量

使用 dotenv()

use dotenvy;

// 加载当前目录的 .env 文件
dotenvy::dotenv()?;

// 读取环境变量
let api_key = std::env::var("API_KEY")?;

使用 dotenv() 的 Result

// 如果 .env 文件不存在,忽略错误
let _ = dotenvy::dotenv();

// 或明确处理
match dotenvy::dotenv() {
    Ok(_) => println!("配置已加载"),
    Err(_) => println!("使用默认配置"),
}

读取环境变量

使用 env::var()

use std::env;

// 返回 Result
let db_url = env::var("DATABASE_URL")?;

// 或使用 unwrap_or
let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string());

使用 env::var_os()

// 返回 Option<OsString>
if let Some(home) = env::var_os("HOME") {
    println!("HOME: {:?}", home);
}

获取系统目录

使用 env::home_dir()

use std::env;

if let Some(home) = env::home_dir() {
    println!("HOME: {}", home.display());
}

使用 home crate

use home;

if let Some(home) = home::home_dir() {
    println!("HOME: {}", home.display());
}

// 获取 Cargo home
if let Some(cargo_home) = home::cargo_home() {
    println!("Cargo HOME: {}", cargo_home.display());
}

构建路径

使用 env!("CARGO_MANIFEST_DIR")

// 编译时获取项目根目录
let manifest_dir = env!("CARGO_MANIFEST_DIR");

// 构建相对路径
let data_path = format!("{}/data/data.txt", manifest_dir);

使用 PathBuf

use std::path::PathBuf;
use std::env;

let manifest_dir = env!("CARGO_MANIFEST_DIR");

// 推荐方式
let data_path = PathBuf::from(manifest_dir)
    .join("data")
    .join("data.txt");

println!("数据文件:{}", data_path.display());

获取当前目录

使用 env::current_dir()

use std::env;

let current_dir = env::current_dir()?;
println!("当前目录:{}", current_dir.display());

获取可执行文件目录

use std::env;

let exe_path = env::current_exe()?;
let exe_dir = exe_path.parent().unwrap();
println!("可执行文件目录:{}", exe_dir.display());

常见错误

错误 1: .env 文件不存在

dotenvy::dotenv()?;  // ❌ 如果 .env 不存在会报错

错误信息:

path not found

修复方法:

let _ = dotenvy::dotenv();  // ✅ 忽略错误

错误 2: 环境变量未设置

let db_url = env::var("DATABASE_URL")?;
// ❌ 如果未设置会报错

错误信息:

environment variable not found

修复方法:

// 提供默认值
let db_url = env::var("DATABASE_URL")
    .unwrap_or_else(|_| "sqlite::memory:".to_string());

错误 3: 路径拼接错误

let path = env!("CARGO_MANIFEST_DIR") + "/data/data.txt";
// ❌ 字符串拼接,不跨平台

修复方法:

use std::path::PathBuf;

let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
    .join("data")
    .join("data.txt");  // ✅ 使用 PathBuf

动手练习

练习 1: 加载 .env 文件

use dotenvy;
use std::env;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // TODO: 加载 .env 文件
    // TODO: 读取 API_KEY 环境变量
    // TODO: 打印结果
}
点击查看答案
dotenvy::dotenv()?;

let api_key = env::var("API_KEY")?;
println!("API Key: {}", api_key);

练习 2: 获取系统目录

use std::env;

fn main() {
    // TODO: 获取 HOME 目录
    // TODO: 获取当前目录
    // TODO: 打印结果
}
点击查看答案
if let Some(home) = env::home_dir() {
    println!("HOME: {}", home.display());
}

let current = env::current_dir().unwrap();
println!("当前目录:{}", current.display());

练习 3: 构建相对路径

use std::path::PathBuf;

fn main() {
    // TODO: 使用 CARGO_MANIFEST_DIR
    // TODO: 构建 data/config.json 路径
    // TODO: 打印路径
}
点击查看答案
let manifest = env!("CARGO_MANIFEST_DIR");

let config_path = PathBuf::from(manifest)
    .join("data")
    .join("config.json");

println!("配置文件:{}", config_path.display());

故障排查 (FAQ)

Q: .env 文件应该放在哪里?

A:

  • 开发环境: 项目根目录
  • 生产环境: 使用系统环境变量
  • 不要提交: .env 文件应该加入 .gitignore

Q: 如何在测试中使用 .env?

A:

#[cfg(test)]
mod tests {
    #[test]
    fn test_with_env() {
        let _ = dotenvy::dotenv();
        // 测试代码
    }
}

Q: 如何覆盖 .env 中的值?

A:

# 系统环境变量优先级更高
export DATABASE_URL="postgres://prod/db"
# 会覆盖 .env 中的值

知识扩展

.env 文件示例

# 数据库配置
DATABASE_URL=postgres://localhost/mydb
DATABASE_POOL_SIZE=10

# API 配置
API_KEY=your_secret_key
API_TIMEOUT=30

# 应用配置
RUST_LOG=debug
PORT=8080
DEBUG=true

条件加载

// 根据环境加载不同配置
if cfg!(debug_assertions) {
    let _ = dotenvy::from_filename(".env.development");
} else {
    let _ = dotenvy::from_filename(".env.production");
}

最佳实践

// config.rs
use dotenvy;
use std::env;

pub struct Config {
    pub database_url: String,
    pub port: u16,
}

impl Config {
    pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
        let _ = dotenvy::dotenv();
        
        Ok(Config {
            database_url: env::var("DATABASE_URL")
                .unwrap_or_else(|_| "sqlite::memory:".to_string()),
            port: env::var("PORT")
                .unwrap_or_else(|_| "3000".to_string())
                .parse()?,
        })
    }
}

小结

核心要点

  1. dotenvy: 从 .env 文件加载环境变量
  2. env::var(): 读取环境变量
  3. home_dir(): 获取用户主目录
  4. CARGO_MANIFEST_DIR: 项目根目录
  5. PathBuf: 构建跨平台路径

关键术语

  • .env File: 环境变量配置文件
  • Environment Variable: 环境变量
  • PathBuf: 路径缓冲区
  • Manifest Dir: 清单目录(项目根目录)

术语表

English中文
Environment Variable环境变量
.env File.env 文件
PathBuf路径缓冲区
Manifest Directory清单目录
Home Directory主目录

知识检查

快速测验(答案在下方):

  1. .env 文件应该在什么时候加载?

  2. 环境变量和配置文件的区别?

  3. 如何处理不同环境的配置?

点击查看答案与解析
  1. 程序启动时,在读取环境变量之前
  2. 环境变量是进程级别的,配置文件是文件持久化的
  3. 使用 .env.development, .env.production 或配置管理库

关键理解: .env 文件不应提交到版本控制(包含敏感信息)。

继续学习

前一章: 内存映射
下一章: 字节处理

相关章节:

返回: 高级进阶


完整示例: dotenv_sample.rs

字节处理

开篇故事

想象你要处理二进制数据(图片、音频、网络数据包)。直接操作字节数组就像用手抓沙子——容易散落。bytes 库就像容器——帮你安全、高效地管理字节数据。


本章适合谁

如果你需要处理二进制数据(网络编程、文件处理、加密),本章适合你。bytes 是高性能字节处理的标准库。


你会学到什么

完成本章后,你可以:

  1. 创建 Bytes 和 BytesMut
  2. 分割和合并字节
  3. 实现 Buf 和 BufMut trait
  4. 使用 Base64 编解码
  5. 使用位向量 (BitVec)

前置要求

  • 变量与表达式 - 基础语法
  • 数据类型 - 数据类型基础
  • 不安全代码 - unsafe 基础(可选)

依赖安装

运行以下命令安装所需依赖:

cargo add bytes

第一个例子

最简单的 Bytes 使用:

use bytes::{Bytes, BytesMut, BufMut};

fn main() {
    // 从字符串创建 Bytes
    let mut mem = Bytes::from("Hello world");
    
    // 切片操作(零拷贝)
    let a = mem.slice(0..5);
    assert_eq!(a, "Hello");
    
    // 分割操作
    let b = mem.split_to(6);
    assert_eq!(b, "Hello ");
    assert_eq!(mem, "world");
    
    println!("a: {}, b: {}, mem: {}", a, b, mem);
}

完整示例: bytes_sample.rs


原理解析

bytes 特性

bytes 是字节处理库

  • ✅ 零拷贝操作
  • ✅ 高性能
  • ✅ Buf/BufMut trait
  • ✅ 线程安全

Bytes 和 BytesMut

Bytes (不可变)

use bytes::Bytes;

// 从字符串创建
let bytes = Bytes::from("Hello");

// 从字节数组创建
let bytes = Bytes::from(&b"Hello"[..]);

// 切片(零拷贝)
let slice = bytes.slice(0..5);

BytesMut (可变)

use bytes::BytesMut;

// 创建可变缓冲区
let mut buf = BytesMut::with_capacity(1024);

// 写入数据
buf.put(&b"Hello"[..]);
buf.put_u16(1234);

// 转换为 Bytes
let bytes = buf.freeze();

分割操作

使用 split()

use bytes::BytesMut;

let mut buf = BytesMut::with_capacity(1024);
buf.put(&b"hello world"[..]);

// 分割并获取前半部分
let a = buf.split();
assert_eq!(a, b"hello world"[..]);

使用 split_to()

let mut buf = BytesMut::from(&b"hello world"[..]);

// 分割前 6 个字节
let b = buf.split_to(6);
assert_eq!(b, b"hello "[..]);
assert_eq!(buf, b"world"[..]);

Buf Trait

读取数据

use bytes::{Buf, Bytes};

let mut buf = Bytes::from(&b"hello"[..]);

// 读取字节
let byte = buf.get_u8();

// 读取 u16 (大端)
let val = buf.get_u16();

// 检查剩余字节
if buf.has_remaining() {
    println!("还有 {} 字节", buf.remaining());
}

Base64 编解码

使用 base64 crate

use base64::{Engine as _, engine::general_purpose::STANDARD};

// 编码
let encoded = STANDARD.encode("Hello World!");
println!("Base64: {}", encoded);

// 解码
let decoded = STANDARD.decode(&encoded)?;
println!("解码:{}", String::from_utf8_lossy(&decoded));

使用 URL 安全编码

use base64::{Engine as _, engine::general_purpose::URL_SAFE};

let encoded = URL_SAFE.encode("Hello+World/");
println!("URL Safe: {}", encoded);

BitVec 位向量

使用 bitvec crate

use bitvec::prelude::*;

// 创建位向量
let mut bv = bitvec![0, 0, 1, 1, 0, 1, 0, 1];

// 访问位
if bv[2] {
    println!("第 3 位是 1");
}

// 修改位
bv.set(0, true);

// 转换为字节
let bytes: Vec<u8> = bv.as_bytes().to_vec();

常见错误

错误 1: 越界访问

use bytes::Bytes;

let bytes = Bytes::from(&b"hello"[..]);
let slice = bytes.slice(0..10);  // ❌ 越界

错误信息:

range out of bounds

修复方法:

let slice = bytes.slice(0..5.min(bytes.len()));  // ✅ 检查边界

错误 2: 忘记检查剩余字节

use bytes::Buf;

let mut buf = Bytes::from(&b"hello"[..]);

while buf.has_remaining() {
    let byte = buf.get_u8();
    // ❌ 如果数据不足会 panic
}

修复方法:

while buf.remaining() >= 1 {  // ✅ 检查足够字节
    let byte = buf.get_u8();
}

错误 3: Base64 解码错误

use base64::{Engine as _, engine::general_purpose::STANDARD};

let invalid = "Invalid!Base64";
let decoded = STANDARD.decode(invalid)?;  // ❌ 可能失败

错误信息:

Invalid last symbol

修复方法:

match STANDARD.decode(invalid) {
    Ok(decoded) => println!("解码:{}", String::from_utf8_lossy(&decoded)),
    Err(e) => eprintln!("解码失败:{}", e),
}

动手练习

练习 1: 创建和分割 Bytes

use bytes::{Bytes, BytesMut};

fn main() {
    // TODO: 创建 BytesMut
    // TODO: 写入 "Hello World"
    // TODO: 分割前 5 个字节
    // TODO: 打印结果
}
点击查看答案
let mut buf = BytesMut::with_capacity(1024);
buf.put(&b"Hello World"[..]);

let hello = buf.split_to(5);
assert_eq!(hello, b"Hello"[..]);
assert_eq!(buf, b" World"[..]);

练习 2: Base64 编解码

use base64::{Engine as _, engine::general_purpose::STANDARD};

fn main() {
    let original = "Hello Rust!";
    
    // TODO: 编码为 Base64
    // TODO: 解码回原始字符串
    // TODO: 验证结果
}
点击查看答案
let encoded = STANDARD.encode(original);
println!("Base64: {}", encoded);

let decoded = STANDARD.decode(&encoded).unwrap();
assert_eq!(original, String::from_utf8_lossy(&decoded));

练习 3: 位向量操作

use bitvec::prelude::*;

fn main() {
    // TODO: 创建 8 位位向量
    // TODO: 设置第 0、2、4、6 位为 1
    // TODO: 转换为字节
}
点击查看答案
let mut bv = bitvec![0; 8];
bv.set(0, true);
bv.set(2, true);
bv.set(4, true);
bv.set(6, true);

let bytes: Vec<u8> = bv.as_bytes().to_vec();
println!("字节:{:02x?}", bytes);

故障排查 (FAQ)

Q: Bytes 和 Vec 有什么区别?

A:

  • Bytes: 零拷贝,适合网络/IO
  • Vec: 可修改,适合一般用途
  • 性能: Bytes 在分割/切片时性能更优

Q: 什么时候使用 Buf trait?

A:

  • 解析二进制协议
  • 网络数据读取
  • 文件解析

Q: BitVec 和普通 Vec 有什么区别?

A:

  • BitVec: 每 bit 存储一个布尔值(节省空间)
  • Vec: 每字节存储一个布尔值
  • 性能: BitVec 空间效率更高

知识扩展

字节序转换

use byteorder::{BigEndian, ReadBytesExt};
use std::io::Cursor;

let data = vec![0x00, 0x00, 0x03, 0xe8];
let mut cursor = Cursor::new(data);

let val = cursor.read_u32::<BigEndian>()?;
assert_eq!(val, 1000);

性能优化

use bytes::BytesMut;

// 预分配容量
let mut buf = BytesMut::with_capacity(4096);

// 避免多次分配
buf.reserve(1024);

网络编程应用

use bytes::{Bytes, BytesMut, BufMut};

// 构建网络消息
let mut msg = BytesMut::with_capacity(1024);
msg.put_u32(100);  // 消息长度
msg.put(&b"Hello"[..]);  // 消息内容

// 发送
socket.send(&msg.freeze()).await?;

小结

核心要点

  1. Bytes: 零拷贝字节容器
  2. BytesMut: 可写字节缓冲区
  3. Buf/BufMut: 字节读取/写入 trait
  4. Base64: 二进制到文本编码
  5. BitVec: 位级别操作

关键术语

  • Bytes: 字节类型
  • Buf: 缓冲区 trait
  • Zero-copy: 零拷贝
  • Base64: Base64 编码
  • BitVec: 位向量

术语表

English中文
Bytes字节
Buffer缓冲区
Zero-copy零拷贝
Base64Base64 编码
BitVec位向量
Endianness字节序

延伸阅读

学习完字节处理后,你可能还想了解:

选择建议:

知识检查

快速测验(答案在下方):

  1. &[u8]Vec<u8> 的区别?

  2. 如何高效地拼接字节缓冲区?

  3. Bytes crate 的优势是什么?

点击查看答案与解析
  1. &[u8] 是借用切片,Vec<u8> 是拥有的向量
  2. 使用 Vec::extend 或预分配容量
  3. 引用计数、零拷贝克隆、高效分割

关键理解: 字节处理是网络和系统编程的基础。

继续学习

前一章: 环境变量
下一章: Cow 类型

相关章节:

返回: 高级进阶


完整示例: bytes_sample.rs

Cow 类型

开篇故事

想象你有一本书要修改。传统方式是:复印整本书 → 修改复印件 → 使用。Cow 就像是:如果需要修改才复印,如果不需要修改直接看原书。Cow 类型就是这样的智能类型——按需克隆。


本章适合谁

如果你需要优化内存使用(只读时借用,修改时克隆),本章适合你。Cow 是 Rust 特有的零成本抽象。


你会学到什么

完成本章后,你可以:

  1. 理解 Cow 类型概念
  2. 使用 Cow::Borrowed 和 Cow::Owned
  3. 使用 to_mut() 按需克隆
  4. 优化字符串处理
  5. 实现零拷贝优化

前置要求

  • 所有权 - 所有权基础
  • 借用 - 借用基础
  • 枚举 - 枚举类型

第一个例子

最简单的 Cow 使用:

use std::borrow::Cow;

fn filter_profanity(input: &str) -> Cow<str> {
    if input.contains("badword") {
        // 需要修改:克隆并返回 Owned
        let filtered = input.replace("badword", "****");
        Cow::Owned(filtered)
    } else {
        // 不需要修改:直接返回 Borrowed(零分配)
        Cow::Borrowed(input)
    }
}

fn main() {
    let s1 = "Hello, world!";
    let res1 = filter_profanity(s1);
    println!("结果 1: {} (owned: {})", res1, matches!(res1, Cow::Owned(_)));
    
    let s2 = "This is a badword!";
    let res2 = filter_profanity(s2);
    println!("结果 2: {} (owned: {})", res2, matches!(res2, Cow::Owned(_)));
}

完整示例: cow_sample.rs


原理解析

Cow 特性

Cow (Clone-on-Write) 是写时克隆类型

  • ✅ 只读时零拷贝
  • ✅ 修改时自动克隆
  • ✅ 智能优化
  • ✅ 零成本抽象

Cow 枚举

Cow 是一个枚举

enum Cow<'a, B: ToOwned> {
    Borrowed(&'a B),      // 借用
    Owned(<B as ToOwned>::Owned),  // 拥有
}

对于 Cow

  • Cow::Borrowed(&str): 借用字符串切片
  • Cow::Owned(String): 拥有 String

创建 Cow

使用 Borrowed

use std::borrow::Cow;

let borrowed: Cow<str> = Cow::Borrowed("Hello");
// 零分配,直接借用

使用 Owned

let owned: Cow<str> = Cow::Owned(String::from("Hello"));
// 分配内存,拥有数据

to_mut() 方法

按需克隆

use std::borrow::Cow;

let mut cow: Cow<str> = Cow::Borrowed("original");

// 第一次调用 to_mut():如果是 Borrowed,会克隆
cow.to_mut().make_ascii_uppercase();

// 第二次调用:已经是 Owned,直接返回引用
cow.to_mut().push_str("!!!");

println!("{}", cow);  // 输出:ORIGINAL!!!

性能优势

只读场景

fn process_data(data: &[u8]) -> Cow<[u8]> {
    // 只读场景:直接返回借用
    Cow::Borrowed(data)
}

// 零分配
let data = vec![1, 2, 3, 4];
let result = process_data(&data);

修改场景

fn process_data_mut(data: &[u8], modify: bool) -> Cow<[u8]> {
    let mut cow = Cow::Borrowed(data);
    
    if modify {
        // 修改场景:克隆并修改
        cow.to_mut().push(0xFF);
    }
    
    cow
}

实际应用

页面数据处理

use std::borrow::Cow;

struct Page {
    id: u64,
    data: Vec<u8>,
}

fn process_page_data<'a>(
    page_data: &'a [u8],
    is_writable: bool
) -> Cow<'a, [u8]> {
    let mut cow = Cow::Borrowed(page_data);
    
    if is_writable {
        // to_mut() 会检查:如果是 Borrowed,则克隆
        // 如果已经是 Owned,直接返回引用
        let mutable_data = cow.to_mut();
        mutable_data[0] = 0xFF;  // 修改标记位
    }
    
    cow
}

fn main() {
    let disk_data = vec![0u8; 4096];  // 模拟磁盘数据
    
    // 只读场景:完全不分配内存
    let read_only = process_page_data(&disk_data, false);
    println!("只读:{:?}", read_only[0]);
    
    // 写入场景:在 to_mut() 被调用时发生一次 4KB 拷贝
    let writable = process_page_data(&disk_data, true);
    println!("可写:{:?}", writable[0]);
}

常见错误

错误 1: 忘记使用 to_mut()

use std::borrow::Cow;

let mut cow: Cow<str> = Cow::Borrowed("hello");
cow.push_str(" world");  // ❌ 编译错误

错误信息:

no method named `push_str` found for enum `Cow`

修复方法:

cow.to_mut().push_str(" world");  // ✅ 使用 to_mut()

错误 2: 生命周期错误

fn create_cow() -> Cow<str> {
    let s = String::from("hello");
    Cow::Borrowed(&s)  // ❌ s 会被释放
}

错误信息:

borrowed value does not live long enough

修复方法:

fn create_cow() -> Cow<'static, str> {
    Cow::Borrowed("hello")  // ✅ 字符串字面量有 'static 生命周期
}

错误 3: 不必要的克隆

fn process(data: &str) -> Cow<str> {
    let mut cow = Cow::Borrowed(data);
    cow.to_mut();  // ❌ 不必要的克隆
    cow
}

修复方法:

fn process(data: &str) -> Cow<str> {
    Cow::Borrowed(data)  // ✅ 只在需要时克隆
}

动手练习

练习 1: 创建 Cow

use std::borrow::Cow;

fn main() {
    // TODO: 创建 Borrowed Cow
    // TODO: 创建 Owned Cow
    // TODO: 打印结果
}
点击查看答案
let borrowed: Cow<str> = Cow::Borrowed("Hello");
let owned: Cow<str> = Cow::Owned(String::from("World"));

println!("Borrowed: {}", borrowed);
println!("Owned: {}", owned);

练习 2: 使用 to_mut()

use std::borrow::Cow;

fn main() {
    let mut cow: Cow<str> = Cow::Borrowed("original");
    
    // TODO: 使用 to_mut() 转换为大写
    // TODO: 添加感叹号
    // TODO: 打印结果
}
点击查看答案
cow.to_mut().make_ascii_uppercase();
cow.to_mut().push_str("!!!");

println!("{}", cow);  // 输出:ORIGINAL!!!

练习 3: 优化字符串处理

use std::borrow::Cow;

fn trim_and_process(input: &str) -> Cow<str> {
    // TODO: 如果字符串有前后空格,克隆并修剪
    // TODO: 如果没有空格,直接返回借用
    // TODO: 返回 Cow<str>
}
点击查看答案
if input.trim() == input {
    Cow::Borrowed(input)  // 无需修改,零分配
} else {
    Cow::Owned(input.trim().to_string())  // 需要修改,克隆
}

故障排查 (FAQ)

Q: Cow 和 Option 有什么区别?

A:

  • Cow: 优化克隆(借用 vs 拥有)
  • Option: 表示可能有值或无值
  • 用途不同

Q: Cow 只能用于 String 吗?

A:

  • 不,Cow 可以用于任何实现 ToOwned trait 的类型
  • 常见:Cow<str>, Cow<[T]>, Cow<Path>

Q: 什么时候使用 Cow?

A:

  • 函数可能返回借用或拥有数据
  • 优化只读场景的性能
  • API 设计:灵活返回类型

知识扩展

Cow<[T]> 用于切片

use std::borrow::Cow;

fn process_slice(data: &[i32]) -> Cow<[i32]> {
    if data.iter().all(|&x| x > 0) {
        // 都是正数,直接返回借用
        Cow::Borrowed(data)
    } else {
        // 有负数,过滤并返回拥有
        let filtered: Vec<i32> = data.iter().copied().filter(|&x| x > 0).collect();
        Cow::Owned(filtered)
    }
}

Cow 用于路径

use std::borrow::Cow;
use std::path::{Path, PathBuf};

fn resolve_path(input: &str) -> Cow<Path> {
    if input.starts_with('/') {
        // 绝对路径,直接借用
        Cow::Borrowed(Path::new(input))
    } else {
        // 相对路径,需要解析
        let full_path = std::env::current_dir().unwrap().join(input);
        Cow::Owned(full_path)
    }
}

性能对比

// 传统方式:总是克隆
fn process_always_clone(s: &str) -> String {
    s.to_uppercase()  // 总是分配
}

// Cow 方式:按需克隆
fn process_cow(s: &str) -> Cow<str> {
    if s.chars().all(|c| c.is_ascii_uppercase()) {
        Cow::Borrowed(s)  // 已经是大写,零分配
    } else {
        Cow::Owned(s.to_uppercase())  // 需要转换,分配
    }
}

小结

核心要点

  1. Cow: 写时克隆类型
  2. Borrowed: 借用,零分配
  3. Owned: 拥有,分配内存
  4. to_mut(): 按需克隆
  5. 零成本: 只读场景无开销

关键术语

  • Cow (Clone-on-Write): 写时克隆
  • Borrowed: 借用变体
  • Owned: 拥有变体
  • Zero-copy: 零拷贝

术语表

English中文
Cow (Clone-on-Write)写时克隆
Borrowed借用
Owned拥有
ToOwned traitToOwned trait
Zero-copy零拷贝

延伸阅读

学习完 Cow 类型后,你可能还想了解:

选择建议:

知识检查

快速测验(答案在下方):

  1. Cow<'a, str> 的三个变体是什么?

  2. 什么时候会发生克隆?

  3. Cow 的使用场景?

点击查看答案与解析
  1. Borrowed(&'a str)Owned(String)
  2. 当需要修改借用数据时
  3. 读多写少场景,如配置处理、字符串处理

关键理解: Cow 是写时复制的零成本抽象。

继续学习

前一章: 字节处理
下一章: 派生宏

相关章节:

返回: 高级进阶


完整示例: cow_sample.rs

进程管理

开篇故事

想象你要运行一个外部程序。传统方式是:手动启动 → 等待完成 → 获取结果。process 库就像是:程序管家——帮你启动、管理、监控外部进程。


本章适合谁

如果你需要在 Rust 程序中运行外部命令、管理子进程,本章适合你。进程管理是系统编程的基础。


你会学到什么

完成本章后,你可以:

  1. 获取当前进程 ID
  2. 启动子进程
  3. 管理进程生命周期
  4. 捕获进程输出
  5. 处理进程错误

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add nix --features process,signal

第一个例子

获取当前进程 ID:

use std::process;

fn main() {
    // 获取当前进程 ID
    let current_pid = process::id();
    
    println!("当前进程 ID: {}", current_pid);
}

完整示例: process_sample.rs


原理解析

process 特性

std::process 是进程管理库

  • ✅ 获取进程信息
  • ✅ 启动子进程
  • ✅ 管理进程生命周期
  • ✅ 捕获输出

获取进程 ID

使用 process::id()

use std::process;

let pid = process::id();
println!("进程 ID: {}", pid);

启动子进程

使用 Command

use std::process::Command;

let output = Command::new("ls")
    .arg("-l")
    .output()
    .expect("Failed to execute command");

println!("输出:{}", String::from_utf8_lossy(&output.stdout));

捕获输出

使用 output()

use std::process::Command;

let output = Command::new("echo")
    .arg("Hello from Rust!")
    .output()
    .expect("Failed");

println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));

进程状态

检查进程状态

use std::process::Command;

let mut child = Command::new("sleep")
    .arg("5")
    .spawn()
    .expect("Failed to spawn");

// 等待进程完成
let status = child.wait().expect("Failed to wait");

println!("进程退出码:{}", status.code().unwrap_or(-1));

常见错误

错误 1: 命令不存在

let output = Command::new("nonexistent_command")
    .output();  // ❌ 命令不存在

错误信息:

No such file or directory (os error 2)

修复方法:

let output = Command::new("ls")  // ✅ 使用存在的命令
    .output();

错误 2: 忘记处理错误

let output = Command::new("ls").output();
println!("{}", output.stdout);  // ❌ output 是 Result

错误信息:

no field `stdout` on type `Result<Output, Error>`

修复方法:

let output = Command::new("ls").output()?;  // ✅ 使用 ? 处理错误

错误 3: 忘记 flush

println!("PID: {}", pid);
// ❌ 如果没有 flush,输出可能不会立即显示

修复方法:

use std::io::Write;

println!("PID: {}", pid);
std::io::stdout().flush()?;  // ✅ 立即刷新

动手练习

练习 1: 获取进程信息

use std::process;

fn main() {
    // TODO: 获取当前进程 ID
    // TODO: 打印 PID
}
点击查看答案
let pid = process::id();
println!("当前进程 ID: {}", pid);

练习 2: 运行外部命令

use std::process::Command;

fn main() {
    // TODO: 运行 "pwd" 命令
    // TODO: 打印输出
}
点击查看答案
let output = Command::new("pwd")
    .output()
    .expect("Failed");

println!("当前目录:{}", String::from_utf8_lossy(&output.stdout));

练习 3: 带参数的命令

use std::process::Command;

fn main() {
    // TODO: 运行 "ls -la" 命令
    // TODO: 打印输出
}
点击查看答案
let output = Command::new("ls")
    .arg("-la")
    .output()
    .expect("Failed");

println!("{}", String::from_utf8_lossy(&output.stdout));

故障排查 (FAQ)

Q: spawn() 和 output() 有什么区别?

A:

  • output(): 等待进程完成,返回输出
  • spawn(): 立即返回,异步管理进程

Q: 如何处理大输出?

A:

let mut child = Command::new("cat")
    .arg("large_file.txt")
    .spawn()?;

// 逐行读取,避免内存溢出
let stdout = child.stdout.take().unwrap();
for line in std::io::BufReader::new(stdout).lines() {
    println!("{}", line?);
}

Q: 如何设置超时?

A:

use std::time::Duration;

let mut child = Command::new("sleep")
    .arg("10")
    .spawn()?;

// 设置 5 秒超时
child.wait_timeout(Duration::from_secs(5))?;

知识扩展

环境变量

use std::process::Command;
use std::env;

let output = Command::new("echo")
    .env("MY_VAR", "Hello")
    .arg("$MY_VAR")
    .output()?;

管道

use std::process::{Command, Stdio};

let child = Command::new("cat")
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .spawn()?;

// 写入 stdin
child.stdin.unwrap().write_all(b"Hello")?;

退出码

let status = child.wait()?;

if status.success() {
    println!("成功");
} else {
    println!("失败,退出码:{}", status.code().unwrap_or(-1));
}

小结

核心要点

  1. process::id(): 获取进程 ID
  2. Command: 启动子进程
  3. output(): 同步执行
  4. spawn(): 异步执行
  5. wait(): 等待进程完成

关键术语

  • Process: 进程
  • PID: 进程 ID
  • Command: 命令
  • Spawn: 派生

术语表

English中文
Process进程
PID进程 ID
Command命令
Spawn派生
Output输出

知识检查

快速测验(答案在下方):

  1. Command::spawn()Command::output() 有什么区别?

  2. 如何向子进程发送数据?

  3. 子进程退出后如何获取退出码?

点击查看答案与解析
  1. spawn() 返回正在运行的子进程,output() 等待完成并返回输出
  2. 使用 stdin(Stdio::piped()) 获取 stdin 句柄
  3. Child::wait() 返回 ExitStatus,使用 .code() 获取退出码

关键理解: spawn() 适合长时间运行的进程,output() 适合一次性命令。

继续学习

前一章: Ollama AI 集成
下一章: 系统信息

相关章节:

返回: 高级进阶


完整示例: process_sample.rs

系统信息

开篇故事

想象你要检查电脑的健康状况。传统方式是:打开各种工具 → 查看 CPU → 查看内存 → 查看进程。sysinfo 库就像是:系统仪表盘——一个库获取所有系统信息。


本章适合谁

如果你需要在 Rust 程序中获取系统信息(CPU、内存、进程),本章适合你。sysinfo 是跨平台系统监控的标准库。


你会学到什么

完成本章后,你可以:

  1. 获取系统信息
  2. 获取内存使用情况
  3. 获取进程列表
  4. 监控特定进程
  5. 获取 CPU 信息

前置要求

  • 结构体 - 结构体基础
  • 错误处理 - 错误处理基础

依赖安装

运行以下命令安装所需依赖:

cargo add sysinfo
cargo add bigdecimal

第一个例子

获取系统信息:

use sysinfo::{System, SystemExt};

fn main() {
    // 创建系统实例
    let mut system = System::new_all();
    
    // 刷新所有信息
    system.refresh_all();
    
    // 获取可用内存
    println!("可用内存:{} MB", system.available_memory() / 1024 / 1024);
    
    // 获取系统信息
    println!("操作系统:{:?}", System::name());
    println!("系统版本:{:?}", System::os_version());
    println!("CPU 架构:{:?}", System::cpu_arch());
}

完整示例: sysinfo_sample.rs


原理解析

sysinfo 特性

sysinfo 是系统信息库

  • ✅ 跨平台支持
  • ✅ 获取系统信息
  • ✅ 获取进程信息
  • ✅ 实时监控

创建系统实例

使用 System::new_all()

use sysinfo::{System, SystemExt};

let mut system = System::new_all();

使用 System::new()

// 只创建,不刷新
let mut system = System::new();

// 手动刷新
system.refresh_memory();
system.refresh_processes();

获取系统信息

获取操作系统信息

use sysinfo::System;

println!("操作系统:{:?}", System::name());
println!("系统版本:{:?}", System::os_version());
println!("CPU 架构:{:?}", System::cpu_arch());
println!("发行版:{:?}", System::distribution_id());
println!("内核版本:{:?}", System::kernel_version());

获取内存信息

获取内存使用情况

use sysinfo::{System, SystemExt};

let mut system = System::new_all();

println!("总内存:{} MB", system.total_memory() / 1024 / 1024);
println!("可用内存:{} MB", system.available_memory() / 1024 / 1024);
println!("已用内存:{} MB", system.used_memory() / 1024 / 1024);

获取进程信息

获取所有进程

use sysinfo::{System, SystemExt, Pid};

let mut system = System::new_all();

for (pid, process) in system.processes() {
    println!("进程 ID: {:?}", pid);
    println!("进程名:{}", process.name());
    println!("CPU 使用:{}%", process.cpu_usage());
    println!("内存:{} MB", process.memory() / 1024 / 1024);
}

监控特定进程

获取特定进程

use sysinfo::{System, SystemExt, Pid, ProcessExt};

let mut system = System::new_all();
let pid = std::process::id();

if let Some(process) = system.process(Pid::from_u32(pid as usize)) {
    println!("当前进程:");
    println!("  名称:{}", process.name());
    println!("  CPU: {}%", process.cpu_usage());
    println!("  内存:{} MB", process.memory() / 1024 / 1024);
    println!("  启动时间:{}", process.start_time());
    println!("  运行时间:{:?}", process.run_time());
}

常见错误

错误 1: 忘记刷新

let mut system = System::new();
println!("{}", system.available_memory());
// ❌ 没有刷新,数据可能过时

修复方法:

let mut system = System::new_all();  // ✅ 创建时刷新
// 或
system.refresh_memory();  // ✅ 手动刷新

错误 2: PID 类型错误

let pid = std::process::id();  // u32
system.process(pid);  // ❌ 需要 Pid 类型

修复方法:

use sysinfo::Pid;

let pid = std::process::id();
system.process(Pid::from_u32(pid as usize));  // ✅ 转换类型

错误 3: 忘记导入 trait

use sysinfo::System;

let mut system = System::new_all();
system.refresh_all();  // ❌ 需要导入 SystemExt

修复方法:

use sysinfo::{System, SystemExt};  // ✅ 导入 trait

动手练习

练习 1: 获取系统信息

use sysinfo::{System, SystemExt};

fn main() {
    let mut system = System::new_all();
    
    // TODO: 打印操作系统名称
    // TODO: 打印系统版本
    // TODO: 打印 CPU 架构
}
点击查看答案
println!("操作系统:{:?}", System::name());
println!("系统版本:{:?}", System::os_version());
println!("CPU 架构:{:?}", System::cpu_arch());

练习 2: 获取内存信息

use sysinfo::{System, SystemExt};

fn main() {
    let mut system = System::new_all();
    
    // TODO: 打印总内存
    // TODO: 打印可用内存
    // TODO: 打印已用内存
}
点击查看答案
println!("总内存:{} MB", system.total_memory() / 1024 / 1024);
println!("可用内存:{} MB", system.available_memory() / 1024 / 1024);
println!("已用内存:{} MB", system.used_memory() / 1024 / 1024);

练习 3: 监控当前进程

use sysinfo::{System, SystemExt, Pid};

fn main() {
    let mut system = System::new_all();
    let pid = std::process::id();
    
    // TODO: 获取当前进程
    // TODO: 打印进程信息(名称、CPU、内存)
}
点击查看答案
if let Some(process) = system.process(Pid::from_u32(pid as usize)) {
    println!("名称:{}", process.name());
    println!("CPU: {}%", process.cpu_usage());
    println!("内存:{} MB", process.memory() / 1024 / 1024);
}

故障排查 (FAQ)

Q: 为什么获取的内存信息不准确?

A:

  • 需要调用 refresh_all() 或 refresh_memory()
  • 数据是快照,会随时间变化

Q: 如何持续监控?

A:

loop {
    system.refresh_processes();
    println!("CPU: {}%", process.cpu_usage());
    std::thread::sleep(Duration::from_secs(1));
}

Q: sysinfo 支持哪些平台?

A:

  • Linux
  • macOS
  • Windows
  • FreeBSD

知识扩展

CPU 信息

use sysinfo::{System, SystemExt};

let mut system = System::new_all();

println!("CPU 核心数:{}", system.cpus().len());

for cpu in system.cpus() {
    println!("CPU {}: {}%", cpu.name(), cpu.cpu_usage());
}

磁盘信息

use sysinfo::{System, SystemExt};

let mut system = System::new_all();

for disk in system.disks() {
    println!("磁盘:{:?}", disk.name());
    println!("总空间:{} GB", disk.total_space() / 1024 / 1024 / 1024);
    println!("可用空间:{} GB", disk.available_space() / 1024 / 1024 / 1024);
}

网络接口

use sysinfo::{System, SystemExt};

let mut system = System::new_all();

for (interface_name, network) in system.networks() {
    println!("接口:{}", interface_name);
    println!("接收:{} bytes", network.total_received());
    println!("发送:{} bytes", network.total_transmitted());
}

小结

核心要点

  1. System: 系统信息
  2. SystemExt: 系统扩展 trait
  3. 进程监控: 获取进程信息
  4. 跨平台: 支持多平台

关键术语

  • System: 系统
  • Process: 进程
  • PID: 进程 ID
  • Memory: 内存

术语表

English中文
System系统
Process进程
PID进程 ID
Memory内存
CPU中央处理器

知识检查

快速测验(答案在下方):

  1. System::new_all() 做了什么?

  2. 如何获取特定进程的信息?

  3. refresh_all()new_all() 的区别?

点击查看答案与解析
  1. 创建 System 实例并刷新所有信息(CPU、内存、进程等)
  2. 使用 system.process(Pid::from_u32(pid))
  3. new_all() = 创建 + 刷新,refresh_all() = 仅刷新已有实例

关键理解: sysinfo 提供跨平台的系统和进程信息。

继续学习

前一章: 进程管理
下一章: 资源嵌入

相关章节:

返回: 高级进阶


完整示例: sysinfo_sample.rs

资源嵌入

开篇故事

想象你要打包应用程序的资源文件(图片、配置文件)。传统方式是:读取文件 → 打包 → 运行时加载。include_dir 就像是:时间胶囊——在编译时就把文件打包进程序,运行时直接使用。


本章适合谁

如果你想在 Rust 程序中嵌入静态资源(配置文件、图片、数据),本章适合你。include_dir 是编译时资源嵌入的标准库。


你会学到什么

完成本章后,你可以:

  1. 理解 include_dir 概念
  2. 嵌入目录到二进制
  3. 访问嵌入的文件
  4. 遍历目录结构
  5. 处理二进制文件

前置要求


第一个例子

最简单的 include_dir 使用:

use include_dir::{include_dir, Dir};

// 在编译时嵌入 assets 目录
static ASSETS: Dir = include_dir!("assets");

fn main() {
    // 获取文件
    let data_file = ASSETS.get_file("data.txt").unwrap();
    
    // 读取文件内容
    let content = std::str::from_utf8(data_file.contents()).unwrap();
    println!("data.txt: {}", content);
    
    // 遍历所有文件
    for file in ASSETS.files() {
        println!("文件:{:?}", file.path());
    }
}

完整示例: includedir_sample.rs


原理解析

include_dir 特性

include_dir 是资源嵌入库

  • ✅ 编译时嵌入
  • ✅ 零运行时开销
  • ✅ 类型安全
  • ✅ 支持目录遍历

嵌入目录

使用 include_dir! 宏

use include_dir::include_dir;

// 嵌入整个目录
static ASSETS: Dir = include_dir!("assets");

编译时检查

  • 目录必须存在
  • 路径相对于 Cargo.toml

访问文件

使用 get_file()

use include_dir::Dir;

static ASSETS: Dir = include_dir!("assets");

// 获取文件
let file = ASSETS.get_file("config.json").unwrap();

// 读取内容(字节)
let bytes = file.contents();

// 读取内容(字符串)
let content = std::str::from_utf8(bytes).unwrap();

遍历目录

遍历文件

use include_dir::Dir;

static ASSETS: Dir = include_dir!("assets");

// 遍历当前目录
for file in ASSETS.files() {
    println!("文件:{:?}", file.path());
}

递归遍历

fn traverse(dir: &Dir) {
    // 遍历文件
    for file in dir.files() {
        println!("文件:{:?}", file.path());
    }
    
    // 递归遍历子目录
    for subdir in dir.dirs() {
        traverse(subdir);
    }
}

traverse(&ASSETS);

包含字节

使用 include_bytes!:

let data = include_bytes!("assets/data.txt");
println!("字节:{:?}", data);

// 转换为字符串
let content = std::str::from_utf8(data).unwrap();
println!("内容:{}", content);

常见错误

错误 1: 路径错误

static ASSETS: Dir = include_dir!("wrong_path");
// ❌ 目录不存在

错误信息:

No such file or directory

修复方法:

static ASSETS: Dir = include_dir!("assets");  // ✅ 正确路径

错误 2: 忘记 unwrap

let file = ASSETS.get_file("data.txt");
let content = file.contents();  // ❌ file 是 Option

错误信息:

no method named `contents` on type `Option`

修复方法:

let file = ASSETS.get_file("data.txt").unwrap();  // ✅ 解包

错误 3: UTF-8 转换错误

let file = ASSETS.get_file("image.png").unwrap();
let content = std::str::from_utf8(file.contents()).unwrap();
// ❌ 二进制文件不是 UTF-8

修复方法:

// 二进制文件直接处理字节
let bytes = file.contents();
// 或检查是否是文本文件
if let Some(content) = file.contents_utf8() {
    println!("{}", content);
} else {
    println!("二进制文件,大小:{} bytes", file.contents().len());
}

动手练习

练习 1: 嵌入目录

use include_dir::{include_dir, Dir};

// TODO: 嵌入 "data" 目录
// static ASSETS: Dir = ...

fn main() {
    // TODO: 打印嵌入的文件数量
}
点击查看答案
static ASSETS: Dir = include_dir!("data");

fn main() {
    println!("嵌入了 {} 个文件", ASSETS.files().count());
}

练习 2: 读取嵌入文件

use include_dir::{include_dir, Dir};

static ASSETS: Dir = include_dir!("assets");

fn main() {
    // TODO: 获取 "config.txt" 文件
    // TODO: 打印文件内容
}
点击查看答案
let file = ASSETS.get_file("config.txt").unwrap();
let content = std::str::from_utf8(file.contents()).unwrap();
println!("内容:{}", content);

练习 3: 递归遍历

use include_dir::{include_dir, Dir};

static ASSETS: Dir = include_dir!("assets");

// TODO: 实现递归遍历函数
fn traverse(dir: &Dir) {
    // TODO: 遍历文件
    // TODO: 递归遍历子目录
}
点击查看答案
fn traverse(dir: &Dir) {
    for file in dir.files() {
        println!("文件:{:?}", file.path());
    }
    
    for subdir in dir.dirs() {
        traverse(subdir);
    }
}

traverse(&ASSETS);

故障排查 (FAQ)

Q: include_dir 和 include_str 有什么区别?

A:

  • include_str: 嵌入单个文本文件
  • include_bytes: 嵌入单个二进制文件
  • include_dir: 嵌入整个目录

Q: 嵌入的文件会增加二进制大小吗?

A:

  • 会,文件内容直接嵌入二进制
  • 适合小文件(配置、模板)
  • 大文件考虑运行时加载

Q: 如何更新嵌入的文件?

A:

  • 重新编译即可
  • 文件内容变化会自动重新编译

知识扩展

条件编译

#[cfg(debug_assertions)]
static ASSETS: Dir = include_dir!("assets/dev");

#[cfg(not(debug_assertions))]
static ASSETS: Dir = include_dir!("assets/prod");

嵌入特定文件

use include_dir::{include_dir, Dir, File};

static CONFIG: &str = include_str!("config.json");
static LOGO: &[u8] = include_bytes!("images/logo.png");

性能优化

// 懒加载
lazy_static::lazy_static! {
    static ref ASSETS: Dir<'static> = include_dir!("assets");
}

小结

核心要点

  1. include_dir: 嵌入目录
  2. 编译时嵌入: 零运行时开销
  3. 类型安全: 编译时检查
  4. 遍历支持: 递归遍历目录
  5. 二进制支持: 支持文本和二进制

关键术语

  • Include Dir: 包含目录
  • Embed: 嵌入
  • Compile-time: 编译时
  • Runtime: 运行时

术语表

English中文
Include Dir包含目录
Embed嵌入
Compile-time编译时
Runtime运行时
Binary二进制

延伸阅读

学习完资源嵌入后,你可能还想了解:

选择建议:

知识检查

快速测验(答案在下方):

  1. include_str!include_bytes! 的区别?

  2. 编译时嵌入的优缺点?

  3. rust-embed crate 的作用?

点击查看答案与解析
  1. include_str! 返回 &strinclude_bytes! 返回 &[u8]
  2. 优点:无需运行时加载,缺点:增加二进制大小
  3. 更强大的嵌入,支持目录和 glob 模式

关键理解: 编译时嵌入适合小型静态资源。

继续学习

前一章: 系统信息
下一章: 对象存储

相关章节:

返回: 高级进阶


完整示例: includedir_sample.rs

Unix Domain Socket (UDS)

开篇故事

想象你在一家大公司工作。如果两个部门需要通信,传统方式是打电话(网络 Socket)——即使他们在同一栋楼里。但更高效的方式是直接走到对方办公室(Unix Domain Socket)——因为他们在同一台机器上,无需经过外部网络。

Unix Domain Socket(UDS)是同一台机器上进程间通信(IPC)的高效方式。它使用文件系统路径作为地址,比 TCP/IP 快得多,因为数据不需要经过网络协议栈。


本章适合谁

如果你想学习:

  • 如何在同一台机器上实现高效的进程间通信
  • Unix Domain Socket 的工作原理
  • 如何设计自定义二进制协议

本章适合你。UDS 是本地 IPC 的首选方案。


你会学到什么

完成本章后,你可以:

  1. 理解 Unix Domain Socket 的核心概念
  2. 使用 std::os::unix::net 创建 UDS 服务端
  3. 使用 std::os::unix::net 创建 UDS 客户端
  4. 实现自定义二进制协议(长度前缀 + 负载)
  5. 使用父进程编排多个子进程

前置要求


依赖安装

UDS 使用 Rust 标准库,无需额外依赖:

use std::os::unix::net::{UnixListener, UnixStream};

第一个例子

最简单的 UDS 服务端:

use std::fs;
use std::os::unix::net::UnixListener;
use std::io;

fn main() -> io::Result<()> {
    let socket_path = "/tmp/hello.socket";
    
    // 清理已存在的 socket 文件
    let _ = fs::remove_file(socket_path);
    
    // 创建并绑定 socket
    let listener = UnixListener::bind(socket_path)?;
    println!("UDS 服务端启动在 {}", socket_path);
    
    // 接受连接
    match listener.accept() {
        Ok((stream, _addr)) => {
            println!("客户端已连接");
            // 处理客户端请求...
        }
        Err(e) => eprintln!("接受连接失败: {}", e),
    }
    
    // 清理
    fs::remove_file(socket_path)?;
    Ok(())
}

💡 注意:此代码需要服务端和客户端配合运行。请使用 cargo run --bin uds_servercargo run --bin uds_client 进行完整测试。

完整示例:


原理解析

UDS 架构概览

┌─────────────────────────────────────────────────────────────────────┐
│                    Unix Domain Socket 架构                           │
│                                                                      │
│   ┌──────────────┐         ┌──────────────┐         ┌──────────────┐│
│   │   Parent     │         │   Server     │         │   Client     ││
│   │  (编排进程)   │────────→│  (UDS 监听)   │←────────│  (UDS 连接)   ││
│   │              │  spawn   │              │  accept  │              ││
│   └──────────────┘         └──────┬───────┘         └──────────────┘│
│                                   │                                  │
│   通信协议:                                                                │
│   ┌─────────────────────────────────────────────────────────────────┐ │
│   │ 4 字节大端长度前缀 + UTF-8 负载                                   │ │
│   │ ┌─────────────┐ ┌───────────────────────────────────────────┐   │ │
│   │ │ Length (4B) │ │ Payload (UTF-8 string)                    │   │ │
│   │ │ 0x00000005  │ │ "hello"                                   │   │ │
│   │ └─────────────┘ └───────────────────────────────────────────┘   │ │
│   └─────────────────────────────────────────────────────────────────┘ │
│                                                                      │
│   Socket 路径: /tmp/hello.socket                                     │
└─────────────────────────────────────────────────────────────────────┘

自定义二进制协议

协议格式

┌─────────────┬──────────────────────────────────────────┐
│ Length (4B) │ Payload (UTF-8 string)                   │
│ 大端序       │ 可变长度                                  │
└─────────────┴──────────────────────────────────────────┘

为什么使用长度前缀?

  • 接收方知道要读取多少字节
  • 避免粘包问题
  • 支持可变长度消息

UDS 服务端实现

use std::fs;
use std::io::{self, Read, Write};
use std::os::unix::net::{UnixListener, UnixStream};

fn handle_client(mut stream: UnixStream) -> io::Result<()> {
    loop {
        // 1. 读取长度前缀(4 字节大端序)
        let mut length_bytes = [0u8; 4];
        stream.read_exact(&mut length_bytes)?;
        let length = u32::from_be_bytes(length_bytes) as usize;

        // 2. 读取负载
        let mut payload = vec![0u8; length];
        stream.read_exact(&mut payload)?;
        let message = String::from_utf8(payload).expect("Invalid UTF-8");

        // 3. 处理消息(反转字符串)
        let response = if message == "done" {
            String::from("ok")
        } else {
            message.chars().rev().collect()
        };

        // 4. 发送响应
        let resp_payload = response.as_bytes();
        stream.write_all(&u32::to_be_bytes(resp_payload.len() as u32))?;
        stream.write_all(resp_payload)?;
        stream.flush()?;

        if message == "done" {
            break;
        }
    }
    Ok(())
}

fn main() -> io::Result<()> {
    let socket_path = "/tmp/hello.socket";
    
    // 清理已存在的 socket 文件
    let _ = fs::remove_file(socket_path);
    
    // 创建并绑定 socket
    let listener = UnixListener::bind(socket_path)?;
    println!("服务端启动在 {}", socket_path);
    
    // 接受连接并处理
    match listener.accept() {
        Ok((stream, _addr)) => handle_client(stream)?,
        Err(e) => eprintln!("接受连接失败: {}", e),
    }
    
    // 清理
    fs::remove_file(socket_path)?;
    Ok(())
}

💡 注意:此代码需要服务端配合运行。请使用 cargo run --bin uds_server 进行完整测试。

UDS 客户端实现

use std::io::{self, Read, Write};
use std::os::unix::net::UnixStream;

fn main() -> io::Result<()> {
    let socket_path = "/tmp/hello.socket";
    let messages = vec!["hello", "world", "rust", "done"];

    // 连接到服务端
    let mut stream = UnixStream::connect(socket_path)?;

    for msg in messages {
        // 1. 发送:长度前缀 + 负载
        let payload = msg.as_bytes();
        stream.write_all(&u32::to_be_bytes(payload.len() as u32))?;
        stream.write_all(payload)?;
        stream.flush()?;

        // 2. 接收:长度前缀 + 响应
        let mut length_bytes = [0u8; 4];
        stream.read_exact(&mut length_bytes)?;
        let length = u32::from_be_bytes(length_bytes) as usize;
        let mut resp_payload = vec![0u8; length];
        stream.read_exact(&mut resp_payload)?;
        let resp_str = String::from_utf8(resp_payload).expect("Invalid UTF-8");
        
        println!("收到响应: {}", resp_str);
    }

    Ok(())
}

💡 注意:此代码需要服务端配合运行。请使用 cargo run --bin uds_client 进行完整测试。

父进程编排

use std::fs;
use std::io;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;

fn main() -> io::Result<()> {
    let socket_path = "/tmp/hello.socket";
    
    // 确保 socket 文件干净
    let _ = fs::remove_file(socket_path);
    
    // 启动服务端
    let mut server = Command::new("./uds_server")
        .stdout(Stdio::inherit())
        .spawn()?;
    
    // 等待服务端绑定
    thread::sleep(Duration::from_millis(500));
    
    // 启动客户端
    let mut client = Command::new("./uds_client")
        .stdout(Stdio::inherit())
        .spawn()?;
    
    // 等待完成
    client.wait()?;
    server.wait()?;
    
    // 清理
    let _ = fs::remove_file(socket_path);
    
    Ok(())
}

💡 注意:此代码需要服务端和客户端配合运行。请使用 cargo run --bin uds_parent 进行完整测试。

UDS vs TCP 对比

特性UDSTCP
通信范围同一台机器跨网络
性能更快(无需网络协议栈)较慢
地址格式文件系统路径IP:Port
安全性文件权限控制防火墙/ACL
适用场景本地 IPC远程通信

常见错误

错误 1: 未清理已存在的 socket 文件

// ❌ 错误:直接绑定,可能失败
let listener = UnixListener::bind("/tmp/hello.socket")?;

// ✅ 正确:先清理
let _ = fs::remove_file("/tmp/hello.socket");
let listener = UnixListener::bind("/tmp/hello.socket")?;

错误 2: 未处理 UTF-8 转换错误

// ❌ 错误:直接 unwrap
let message = String::from_utf8(payload).unwrap();

// ✅ 正确:处理错误
let message = match String::from_utf8(payload) {
    Ok(s) => s,
    Err(e) => {
        eprintln!("Invalid UTF-8: {}", e);
        return;
    }
};

错误 3: 未等待服务端绑定

// ❌ 错误:立即启动客户端
let mut server = Command::new("./uds_server").spawn()?;
let mut client = Command::new("./uds_client").spawn()?; // 可能连接失败!

// ✅ 正确:等待服务端就绪
let mut server = Command::new("./uds_server").spawn()?;
thread::sleep(Duration::from_millis(500)); // 或使用更复杂的就绪检测
let mut client = Command::new("./uds_client").spawn()?;

动手练习

练习 1: 修改协议为 JSON

将当前的字符串反转协议改为 JSON 格式:

// TODO: 定义请求/响应结构体
// #[derive(Serialize, Deserialize)]
// struct Request { action: String, data: String }
// struct Response { result: String }

// TODO: 修改 handle_client 使用 JSON 序列化/反序列化
点击查看答案
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Request { action: String, data: String }

#[derive(Serialize, Deserialize)]
struct Response { result: String }

fn handle_client(mut stream: UnixStream) -> io::Result<()> {
    loop {
        let mut length_bytes = [0u8; 4];
        stream.read_exact(&mut length_bytes)?;
        let length = u32::from_be_bytes(length_bytes) as usize;
        
        let mut payload = vec![0u8; length];
        stream.read_exact(&mut payload)?;
        
        let request: Request = serde_json::from_slice(&payload).expect("Invalid JSON");
        
        let response = Response {
            result: request.data.chars().rev().collect(),
        };
        
        let resp_payload = serde_json::to_vec(&response).expect("Serialization failed");
        stream.write_all(&u32::to_be_bytes(resp_payload.len() as u32))?;
        stream.write_all(&resp_payload)?;
        stream.flush()?;
    }
    Ok(())
}

小结

核心要点

  1. UDS 是同一台机器上进程间通信的高效方式
  2. 自定义协议 使用长度前缀避免粘包问题
  3. 父进程编排 使用 Command 启动和管理子进程
  4. 清理 socket 文件 是必要的,否则绑定会失败

关键术语

English中文说明
Unix Domain SocketUnix 域套接字本地 IPC 机制
Length Prefix长度前缀避免粘包问题
Parent Process父进程编排子进程的进程
Child Process子进程被父进程启动的进程
Big-Endian大端序网络字节序

下一步


术语表

English中文
Unix Domain SocketUnix 域套接字
IPC进程间通信
Length Prefix长度前缀
Payload负载/消息体
Big-Endian大端序
Parent Process父进程
Child Process子进程
Bind绑定
Accept接受连接
Connect连接

完整示例:


继续学习

💡 记住:UDS 比 TCP 快得多,因为数据不需要经过网络协议栈。在同一台机器上的进程间通信,优先选择 UDS!

Stdio IPC (标准输入输出进程间通信)

开篇故事

想象你在教一个小孩学习。你问他问题(写入 stdin),他回答你(读取 stdout)。你们之间不需要复杂的协议,只需要简单的"你说一句,我说一句"。

Stdio IPC 就是这种简单的进程间通信方式——父进程通过管道(pipe)向子进程的标准输入写入数据,从子进程的标准输出读取响应。它比 Unix Domain Socket 更简单,适合轻量级的父子进程通信。


本章适合谁

如果你想学习:

  • 如何使用标准输入输出实现进程间通信
  • 父进程如何启动和管理子进程
  • 管道(pipe)的工作原理

本章适合你。Stdio IPC 是最简单的本地 IPC 方案。


你会学到什么

完成本章后,你可以:

  1. 理解 Stdio IPC 的核心概念
  2. 使用 std::process::Command 启动子进程
  3. 使用 Stdio::piped() 创建管道
  4. 实现父进程与子进程的双向通信
  5. 处理子进程的生命周期

前置要求


依赖安装

Stdio IPC 使用 Rust 标准库,无需额外依赖:

use std::process::{Command, Stdio};
use std::io::{self, BufRead, BufReader, Write};

第一个例子

最简单的 Stdio IPC 示例:

子进程 (stdio_child.rs):

use std::io::{self, BufRead, Write};

fn main() -> io::Result<()> {
    let stdin = io::stdin();
    let handle = stdin.lock();
    let mut stdout = io::stdout();

    // 从 stdin 读取行
    for line in handle.lines() {
        let input = line?;
        if input == "done" {
            break;
        }
        // 处理:转换为大写
        writeln!(stdout, "Received: {}", input.to_uppercase())?;
        stdout.flush()?;
    }

    Ok(())
}

💡 注意:此代码需要父进程配合运行。请使用 cargo run --bin stdio_parent 进行完整测试。

父进程 (stdio_parent.rs):

use std::io::{self, BufRead, BufReader, Write};
use std::process::{Command, Stdio};

fn main() -> io::Result<()> {
    let messages = vec!["hello", "world", "rust", "done"];

    // 启动子进程
    let mut child = Command::new("./stdio_child")
        .stdin(Stdio::piped())  // 管道用于父进程写入
        .stdout(Stdio::piped()) // 管道用于父进程读取
        .spawn()?;

    // 获取子进程的 stdin 和 stdout 句柄
    let mut child_stdin = child.stdin.take().expect("Failed to open child stdin");
    let child_stdout = child.stdout.take().expect("Failed to open child stdout");
    let mut reader = BufReader::new(child_stdout);

    // 与子进程通信
    for msg in messages {
        // 写入子进程的 stdin
        writeln!(child_stdin, "{}", msg)?;
        child_stdin.flush()?;

        // 从子进程的 stdout 读取
        let mut response = String::new();
        reader.read_line(&mut response)?;
        print!("父进程收到: {}", response);
        io::stdout().flush()?;
    }

    // 关闭 stdin 以通知子进程
    drop(child_stdin);

    // 等待子进程完成
    child.wait()?;

    Ok(())
}

💡 注意:此代码需要子进程配合运行。请使用 cargo run --bin stdio_parent 进行完整测试。

运行方式:

cargo run --bin stdio_parent

完整示例:


原理解析

Stdio IPC 架构

┌─────────────────────────────────────────────────────────────────────┐
│                    Stdio IPC 架构                                   │
│                                                                      │
│   ┌──────────────┐         ┌──────────────┐         ┌──────────────┐│
│   │   Parent     │         │    Pipe      │         │    Child     ││
│   │  (父进程)     │────────→│  (管道)       │────────→│  (子进程)     ││
│   │              │  stdin   │              │  stdin   │              ││
│   │              │←────────│              │←────────│              ││
│   │              │  stdout  │              │  stdout  │              ││
│   └──────────────┘         └──────────────┘         └──────────────┘│
│                                                                      │
│   通信协议: 行分隔文本(\n)                                           │
│   ┌─────────────────────────────────────────────────────────────────┐ │
│   │ "hello\n" → 子进程处理 → "Received: HELLO\n" ← 子进程响应       │ │
│   └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

管道工作原理

父进程写入 → stdin pipe → 子进程读取
父进程读取 ← stdout pipe ← 子进程写入

关键概念:
- Stdio::piped() 创建管道
- child.stdin.take() 获取写入句柄
- child.stdout.take() 获取读取句柄
- drop(child_stdin) 关闭管道,通知子进程

父进程实现详解

use std::io::{self, BufRead, BufReader, Write};
use std::process::{Command, Stdio};

fn main() -> io::Result<()> {
    let messages = vec!["hello", "world", "rust", "done"];

    // 1. 启动子进程
    let mut child = Command::new("./stdio_child")
        .stdin(Stdio::piped())   // 创建 stdin 管道
        .stdout(Stdio::piped())  // 创建 stdout 管道
        .spawn()?;

    // 2. 获取句柄
    let mut child_stdin = child.stdin.take().expect("Failed to open child stdin");
    let child_stdout = child.stdout.take().expect("Failed to open child stdout");
    let mut reader = BufReader::new(child_stdout);

    // 3. 通信循环
    for msg in messages {
        // 写入
        writeln!(child_stdin, "{}", msg)?;
        child_stdin.flush()?;

        // 读取
        let mut response = String::new();
        reader.read_line(&mut response)?;
        print!("父进程收到: {}", response);
        io::stdout().flush()?;
    }

    // 4. 关闭 stdin(通知子进程结束)
    drop(child_stdin);

    // 5. 等待子进程完成
    child.wait()?;

    Ok(())
}

💡 注意:此代码需要子进程配合运行。请使用 cargo run --bin stdio_parent 进行完整测试。

子进程实现详解

use std::io::{self, BufRead, Write};

fn main() -> io::Result<()> {
    // 1. 获取 stdin 和 stdout
    let stdin = io::stdin();
    let handle = stdin.lock();
    let mut stdout = io::stdout();

    // 2. 读取-处理-响应循环
    for line in handle.lines() {
        let input = line?;
        
        // 3. 检查结束条件
        if input == "done" {
            break;
        }
        
        // 4. 处理并响应
        writeln!(stdout, "Received: {}", input.to_uppercase())?;
        stdout.flush()?; // 重要:刷新缓冲区
    }

    Ok(())
}

💡 注意:此代码需要父进程配合运行。请使用 cargo run --bin stdio_child 进行完整测试。

Stdio IPC vs UDS 对比

特性Stdio IPCUDS
复杂度简单中等
协议行分隔文本自定义二进制
适用场景父子进程任意进程
性能中等
跨平台✅ 是❌ 仅 Unix

常见错误

错误 1: 忘记刷新缓冲区

// ❌ 错误:未刷新,子进程可能收不到数据
writeln!(child_stdin, "{}", msg)?;

// ✅ 正确:刷新缓冲区
writeln!(child_stdin, "{}", msg)?;
child_stdin.flush()?;

错误 2: 未关闭 stdin 导致子进程挂起

// ❌ 错误:子进程永远等待更多输入
child.wait()?; // 挂起!

// ✅ 正确:先关闭 stdin
drop(child_stdin);
child.wait()?;

错误 3: 未使用 BufReader

// ❌ 错误:直接读取,可能读到不完整的数据
let mut response = String::new();
child_stdout.read_to_string(&mut response)?;

// ✅ 正确:使用 BufReader 按行读取
let mut reader = BufReader::new(child_stdout);
let mut response = String::new();
reader.read_line(&mut response)?;

动手练习

练习 1: 实现 JSON 通信

将当前的行分隔协议改为 JSON 格式:

// TODO: 子进程接收 JSON 请求,返回 JSON 响应
// #[derive(Serialize, Deserialize)]
// struct Request { action: String, data: String }
// struct Response { result: String }
点击查看答案
// 子进程
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Request { action: String, data: String }

#[derive(Serialize, Deserialize)]
struct Response { result: String }

fn main() -> io::Result<()> {
    let stdin = io::stdin();
    let handle = stdin.lock();
    let mut stdout = io::stdout();

    for line in handle.lines() {
        let input = line?;
        if input == "done" { break; }
        
        let request: Request = serde_json::from_str(&input).expect("Invalid JSON");
        let response = Response {
            result: request.data.to_uppercase(),
        };
        
        writeln!(stdout, "{}", serde_json::to_string(&response).unwrap())?;
        stdout.flush()?;
    }
    Ok(())
}

小结

核心要点

  1. Stdio IPC 是最简单的进程间通信方式
  2. 管道 通过 Stdio::piped() 创建
  3. 刷新缓冲区 是必要的,否则数据不会发送
  4. 关闭 stdin 通知子进程结束

关键术语

English中文说明
Stdio标准输入输出stdin/stdout/stderr
Pipe管道进程间通信通道
Parent Process父进程启动子进程的进程
Child Process子进程被启动的进程
Flush刷新将缓冲区数据写入

下一步


术语表

English中文
Stdio标准输入输出
Pipe管道
Parent Process父进程
Child Process子进程
Flush刷新
BufReader缓冲读取器
Spawn启动(进程)
Wait等待(进程结束)

完整示例:


继续学习

💡 记住:Stdio IPC 简单但强大。对于父子进程通信,优先选择 Stdio;对于任意进程间通信,选择 UDS 或 gRPC!

CLI 开发最佳实践

开篇故事

想象你在设计一个瑞士军刀。如果每个工具都混在一起,用户会很难找到需要的功能。但如果每个工具都有清晰的标签、合理的位置,用户就能快速找到并使用。

CLI(命令行界面)工具就是程序的"瑞士军刀"。好的 CLI 工具应该:参数清晰、帮助文档完整、错误信息友好、支持子命令组织功能。在 Rust 生态中,clap 是最流行的 CLI 参数解析库,它让构建专业的 CLI 工具变得简单。


本章适合谁

如果你想学习:

  • 如何使用 clap 构建专业的 CLI 工具
  • 如何设计子命令结构
  • CLI 项目的最佳实践

本章适合你。CLI 开发是 Rust 最常见的应用场景之一。


你会学到什么

完成本章后,你可以:

  1. 使用 clap 解析命令行参数
  2. 设计子命令结构组织复杂功能
  3. 实现 --help--version 支持
  4. 处理 CLI 错误并输出友好信息
  5. 设计符合 Unix 哲学的 CLI 工具

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add clap --features derive
cargo add anyhow

第一个例子

最简单的 CLI 工具:

use clap::Parser;

/// 简单的问候程序
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    /// 要问候的人名
    #[arg(short, long)]
    name: String,

    /// 问候次数
    #[arg(short, long, default_value_t = 1)]
    count: u8,
}

fn main() {
    let args = Args::parse();

    for _ in 0..args.count {
        println!("Hello, {}!", args.name);
    }
}

运行效果:

$ cargo run -- --name World --count 3
Hello, World!
Hello, World!
Hello, World!

$ cargo run -- --help
简单的问候程序

Usage: cli-example [OPTIONS] --name <NAME>

Options:
  -n, --name <NAME>    要问候的人名
  -c, --count <COUNT>  问候次数 [default: 1]
  -h, --help           Print help
  -V, --version        Print version

完整示例:basic.rs


原理解析

CLI 架构概览

┌─────────────────────────────────────────────────────────────────────┐
│                        CLI 工具架构                                  │
│                                                                      │
│   ┌──────────────┐    ┌──────────────┐    ┌──────────────┐           │
│   │   clap       │    │   Business   │    │   Output     │           │
│   │  (参数解析)   │───→│   Logic      │───→│  (输出/错误)  │           │
│   │              │    │  (业务逻辑)   │    │              │           │
│   └──────────────┘    └──────────────┘    └──────────────┘           │
│         │                   │                   │                    │
│   ┌─────┴───────────────────┴───────────────────┴─────┐             │
│   │              Error Handling (anyhow)               │             │
│   │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐│             │
│   │  │   Context   │  │   Backtrace │  │   Exit Code ││             │
│   │  │  (上下文)    │  │  (调用栈)    │  │  (退出码)    ││             │
│   │  └─────────────┘  └─────────────┘  └─────────────┘│             │
│   └───────────────────────────────────────────────────┘             │
└─────────────────────────────────────────────────────────────────────┘

clap 核心概念

1. 派生宏(Derive Macros)

use clap::Parser;

#[derive(Parser)]
#[command(name = "my-app")]
#[command(about = "A great CLI tool")]
struct Cli {
    #[arg(short, long)]
    verbose: bool,
    
    #[arg(short, long, default_value = "config.yaml")]
    config: String,
}

2. 子命令(Subcommands)

use clap::{Parser, Subcommand};

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// 添加新项
    Add { name: String },
    /// 删除项
    Remove { id: u32 },
    /// 列出所有项
    List,
}

3. 嵌套子命令

use clap::{Parser, Subcommand, Args};

#[derive(Subcommand)]
enum CalcCommands {
    /// 加法
    Add { a: i32, b: i32 },
    /// 减法
    Sub { a: i32, b: i32 },
    /// 乘法(使用 Args 结构体)
    Mul(Mul),
}

#[derive(Args)]
struct Mul {
    /// 第一个数
    a: i32,
    /// 第二个数
    b: i32,
}

完整 CLI 项目结构

my-cli-app/
├── Cargo.toml
├── src/
│   ├── main.rs           # CLI 入口,参数解析
│   ├── commands/         # 子命令实现
│   │   ├── mod.rs
│   │   ├── add.rs
│   │   ├── remove.rs
│   │   └── list.rs
│   ├── config.rs         # 配置管理
│   └── error.rs          # 错误类型定义
└── tests/
    └── cli_tests.rs      # CLI 集成测试

完整示例:计算器 CLI

use clap::{Args, Parser, Subcommand};

#[derive(Parser)]
#[command(author="ren", version, about="高级计算工具", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// 问候某人
    Hello { name: String },
    /// 执行计算
    Calc {
        #[command(subcommand)]
        operation: CalcCommands,
    },
}

#[derive(Subcommand)]
enum CalcCommands {
    /// 加法
    Add { a: i32, b: i32 },
    /// 减法
    Sub { a: i32, b: i32 },
    /// 乘法
    Mul(Mul),
    /// 除法
    Div(Div),
}

#[derive(Args)]
struct Mul {
    /// 第一个数
    a: i32,
    /// 第二个数
    b: i32,
}

#[derive(Args)]
struct Div {
    /// 第一个数
    a: i32,
    /// 第二个数
    b: i32,
}

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Commands::Hello { name } => {
            println!("hello to {}", name)
        }
        Commands::Calc { operation } => {
            execute_calc_command(operation);
        }
    }
}

fn execute_calc_command(operation: &CalcCommands) {
    match operation {
        CalcCommands::Add { a, b } => {
            println!("{} + {} = {}", a, b, a + b);
        }
        CalcCommands::Sub { a, b } => {
            println!("{} - {} = {}", a, b, a - b);
        }
        CalcCommands::Mul(Mul { a, b }) => {
            println!("{} * {} = {}", a, b, a * b);
        }
        CalcCommands::Div(Div { a, b }) => {
            if *b == 0 {
                eprintln!("错误:除数不能为 0");
                std::process::exit(1);
            }
            println!("{} / {} = {}", a, b, a / b);
        }
    }
}

运行效果:

$ cargo run -- calc add 5 3
5 + 3 = 8

$ cargo run -- calc mul 4 7
4 * 7 = 28

$ cargo run -- calc div 10 0
错误:除数不能为 0

$ cargo run -- --help
高级计算工具

Usage: calc-cli <COMMAND>

Commands:
  hello  问候某人
  calc   执行计算
  help   Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

完整示例:advance.rs

错误处理最佳实践

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let cli = Cli::parse();
    
    match run(&cli) {
        Ok(_) => Ok(()),
        Err(e) => {
            eprintln!("错误: {:?}", e);
            std::process::exit(1);
        }
    }
}

fn run(cli: &Cli) -> Result<()> {
    // 业务逻辑
    let config = std::fs::read_to_string(&cli.config)
        .with_context(|| format!("无法读取配置文件: {}", cli.config))?;
    
    // ...
    Ok(())
}

日志集成

use tracing_subscriber;

fn main() -> Result<()> {
    // 初始化日志
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
        )
        .init();
    
    let cli = Cli::parse();
    
    if cli.verbose {
        tracing::info!("详细模式已启用");
    }
    
    // ...
    Ok(())
}

CLI 设计原则

Unix 哲学

  1. 每个程序只做一件事
  2. 程序之间通过文本流通信
  3. 快速原型,使用文本流

好的 CLI 特征

特征说明示例
清晰的帮助--help 输出完整用法clap 自动生成
版本信息--version 显示版本clap 自动生成
合理的退出码0=成功,1=错误std::process::exit(1)
友好的错误错误信息包含上下文anyhow 的 context
支持管道从 stdin 读取,向 stdout 写入cat file \| my-cli

常见错误

错误 1: 未提供默认值

// ❌ 错误:必需参数,但用户可能不知道
#[arg(short, long)]
output: String,

// ✅ 正确:提供默认值
#[arg(short, long, default_value = "output.txt")]
output: String,

错误 2: 错误信息不友好

// ❌ 错误:直接 unwrap
let file = std::fs::read_to_string(path).unwrap();

// ✅ 正确:提供上下文
let file = std::fs::read_to_string(path)
    .with_context(|| format!("无法读取文件: {}", path))?;

错误 3: 未处理 SIGINT/SIGTERM

// ❌ 错误:无法优雅退出
loop {
    // 永远运行,无法停止
}

// ✅ 正确:监听信号
use tokio::signal;

signal::ctrl_c().await?;
println!("收到 Ctrl+C,正在关闭...");

动手练习

练习 1: 添加新的子命令

在计算器 CLI 中添加 power 子命令,计算 a 的 b 次方:

// TODO: 在 CalcCommands 枚举中添加 Power 变体
// Power { a: i32, b: i32 }

// TODO: 在 execute_calc_command 中处理 Power
点击查看答案
#[derive(Subcommand)]
enum CalcCommands {
    // ... 其他命令
    /// 幂运算
    Power {
        /// 底数
        a: i32,
        /// 指数
        b: u32,
    },
}

fn execute_calc_command(operation: &CalcCommands) {
    match operation {
        // ... 其他处理
        CalcCommands::Power { a, b } => {
            println!("{} ^ {} = {}", a, b, a.pow(*b));
        }
    }
}

小结

核心要点

  1. clap 是 Rust 最流行的 CLI 参数解析库
  2. 派生宏 让参数定义变得简单
  3. 子命令 帮助组织复杂功能
  4. 错误处理 使用 anyhow 提供友好上下文
  5. Unix 哲学 指导 CLI 设计

关键术语

English中文说明
CLI命令行界面文本交互界面
Subcommand子命令嵌套命令
Argument参数命令行输入
Flag标志布尔参数(如 --verbose)
Option选项带值的参数(如 --name World)

下一步


术语表

English中文
CLI命令行界面
Subcommand子命令
Argument参数
Flag标志
Option选项
Parse解析
Derive派生
Context上下文
Exit Code退出码

完整示例:


继续学习

💡 记住:好的 CLI 工具应该像好的 API 一样设计——清晰、一致、友好。使用 clap 的派生宏,让参数定义变得简单!

Rust 消除的问题列表

开篇故事

想象你从 C++ 迁移到 Rust。在 C++ 中,你需要时刻担心:内存泄漏、悬垂指针、数据竞争、缓冲区溢出……这些 bug 可能导致崩溃、安全漏洞,甚至被黑客利用。

Rust 的创新在于:它在编译时就防止了这些问题。你不需要在运行时担心,因为编译器已经帮你检查过了。

本章列出了 Rust 在编译时消除的所有常见问题,让你了解 Rust 相比其他语言的安全优势。


Rust 消除的问题完整列表

内存安全问题

问题C 有吗?C++ 有吗?Rust 如何防止编译时机制
缓冲区溢出✅ 是✅ 是边界检查Vec 自动检查索引
悬垂指针✅ 是✅ 是生命周期检查借用检查器
Use-After-Free✅ 是✅ 是所有权系统移动语义
双重释放✅ 是✅ 是所有权系统每个值只有一个所有者
内存泄漏✅ 是⚠️ 部分Drop trait自动清理
未初始化内存✅ 是⚠️ 部分必须初始化编译器强制
空指针解引用✅ 是✅ 是Option<T>null 关键字
数据竞争✅ 是✅ 是所有权 + Send/Sync编译时检查
Iterator 失效✅ 是✅ 是借用规则编译时阻止
释放后使用✅ 是✅ 是生命周期系统编译时检查

并发安全问题

问题C 有吗?C++ 有吗?Rust 如何防止编译时机制
数据竞争✅ 是✅ 是可变借用独占编译时检查
死锁✅ 是✅ 是不直接防止需要设计模式
竞态条件✅ 是✅ 是不可变性默认mut 显式声明
线程不安全共享✅ 是✅ 是Send/Sync trait编译时检查

类型安全问题

问题C 有吗?C++ 有吗?Rust 如何防止编译时机制
类型转换错误✅ 是⚠️ 部分显式转换as 关键字
整数溢出✅ 是⚠️ 部分Debug 模式 panic编译时/运行时检查
符号溢出✅ 是✅ 是显式类型类型系统
未定义行为✅ 是✅ 是unsafe 标记显式声明

具体案例分析

案例 1: 缓冲区溢出

C 代码(有漏洞)

void process_input(char* input) {
    char buffer[64];
    strcpy(buffer, input);  // 😱 如果 input > 64 字节,溢出!
}

Rust 代码(安全)

#![allow(unused)]
fn main() {
fn process_input(input: &str) {
    let mut buffer = [0u8; 64];
    // Rust 会检查长度,超出会 panic 而不是溢出
    if input.len() <= 64 {
        buffer[..input.len()].copy_from_slice(input.as_bytes());
    }
}
}

案例 2: 悬垂指针

C++ 代码(有漏洞)

int* get_pointer() {
    int x = 42;
    return &x;  // ❌ 返回局部变量的指针!
}  // x 在这里被销毁,指针悬垂

Rust 代码(编译时阻止)

#![allow(unused)]
fn main() {
fn get_pointer() -> &i32 {
    let x = 42;
    &x  // ❌ 编译错误!
}  // 编译器:cannot return reference to local variable `x`
}

案例 3: Use-After-Free

C++ 代码(有漏洞)

std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = std::move(ptr);
std::cout << *ptr;  // ❌ 编译通过!但运行时 UB

Rust 代码(编译时阻止)

#![allow(unused)]
fn main() {
let vec = vec![1, 2, 3];
let vec2 = vec;           // 移动 — vec 被消耗
// vec.len();             // ❌ 编译错误:value used after move
}

案例 4: 数据竞争

C++ 代码(有漏洞)

std::vector<int> data = {1, 2, 3};

// 线程 1
std::thread t1([&]() {
    for (int& x : data) x *= 2;  // 写入
});

// 线程 2
std::thread t2([&]() {
    for (int x : data) std::cout << x;  // 读取
});
// 😱 数据竞争!结果不确定

Rust 代码(编译时阻止)

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];

// 线程 1:可变借用
let t1 = std::thread::spawn(|| {
    for x in &mut data { *x *= 2; }
});

// 线程 2:不可变借用
let t2 = std::thread::spawn(|| {
    for x in &data { println!("{}", x); }
});
// ❌ 编译错误:cannot borrow `data` as immutable because it is also borrowed as mutable
}

案例 5: Iterator 失效

C++ 代码(有漏洞)

std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    if (*it % 2 == 0) {
        vec.erase(it);  // 😱 iterator 失效!
    }
}

Rust 代码(编译时阻止或安全 API)

#![allow(unused)]
fn main() {
let mut vec = vec![1, 2, 3, 4, 5];

// ❌ 编译错误:cannot borrow `vec` as mutable because it is also borrowed as immutable
// for x in &vec {
//     if x % 2 == 0 {
//         vec.retain(|&y| y != *x);
//     }
// }

// ✅ 正确:使用 retain
vec.retain(|&x| x % 2 != 0);
}

工业界影响

真实案例:Microsoft 的 CVE 统计

Microsoft 发现 70% 的 CVE(安全漏洞)是内存安全问题。如果这些代码用 Rust 编写,这些漏洞在编译时就会被阻止。

真实案例:Google 的 Android

Google 发现 Android 中 ~70% 的安全漏洞是内存安全问题。他们正在用 Rust 重写关键组件。

真实案例:AWS Firecracker

AWS 用 Rust 构建 Firecracker(微虚拟机),大幅减少内存安全漏洞,启动时间 < 125ms(比 QEMU 快 10 倍),内存占用 < 5MB。


总结

Rust 通过编译时检查消除了以下类别的问题:

类别问题数量Rust 消除率
内存安全10+~95%
并发安全4+~90%
类型安全4+~95%
总计18+~93%

💡 记住:Rust 不是完美的——它不能防止所有 bug(如逻辑错误、死锁)。但它消除了最常见的、最危险的安全漏洞类别。


术语表

English中文
Buffer Overflow缓冲区溢出
Dangling Pointer悬垂指针
Use-After-Free释放后使用
Double Free双重释放
Data Race数据竞争
Null Pointer Dereference空指针解引用
Iterator InvalidationIterator 失效
Undefined Behavior (UB)未定义行为

完整示例:crates/awesome/src/


继续学习

💡 记住:Rust 的编译时检查不是限制你,而是保护你。每次编译器报错,它都在帮你防止一个潜在的运行时 bug!

原子类型 (Atomic Types)

开篇故事

想象你在银行柜台办理业务。如果只有一个柜台,所有人必须排队等待(Mutex 锁)。如果银行开了多个窗口,并且有一个电子显示屏显示当前排队号码,每个人都可以查看号码并决定何时去办理,而不需要死等。

Rust 的原子类型就像那个电子显示屏。它们允许你在多线程环境下安全地共享和修改数据,而不需要加锁。这是实现高性能并发编程的关键。


本章适合谁

如果你已经理解了基本的并发概念(如线程、Mutex),现在想进一步提升并发性能,或者对无锁编程感兴趣,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 理解原子操作的概念
  2. 使用 std::sync::atomic 模块中的原子类型
  3. 理解内存序 (Memory Ordering) 的基本概念
  4. 对比原子类型与 Mutex 的性能差异
  5. 实现简单的无锁计数器

前置要求


第一个例子

使用 AtomicUsize 实现线程安全的计数器:

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    let counter = Arc::new(AtomicUsize::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // 原子增加
            counter_clone.fetch_add(1, Ordering::Relaxed);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("最终计数:{}", counter.load(Ordering::Relaxed));
}

发生了什么?

  • AtomicUsize: 一个可以安全跨线程修改的整数。
  • fetch_add: 原子地增加数值。
  • Ordering::Relaxed: 指定内存序,这里只关心原子性,不关心顺序。

原理解析

1. 什么是原子操作?

原子操作是不可中断的操作。在多线程环境下,要么操作完全完成,要么完全没发生,不会出现中间状态。

对比 Mutex:

特性原子类型 (Atomic)互斥锁 (Mutex)
性能极高 (硬件指令)较低 (系统调用)
粒度单个值任意数据结构
阻塞无锁 (Non-blocking)会阻塞线程
复杂性较高 (需理解内存序)较低 (API 简单)

2. 常用原子类型

Rust 提供了一系列原子类型:

  • 整数: AtomicI8, AtomicI16, AtomicI32, AtomicI64, AtomicIsize
  • 无符号整数: AtomicU8, AtomicU16, AtomicU32, AtomicU64, AtomicUsize
  • 布尔: AtomicBool
  • 指针: AtomicPtr<T>

3. 内存序 (Memory Ordering)

内存序定义了原子操作如何影响内存的可见性。这是原子类型中最难理解的部分。

常见内存序:

  1. Relaxed: 只保证原子性,不保证顺序。适用于计数器。
  2. Acquire: 保证此操作之后的读写不会被重排到操作之前。用于获取锁。
  3. Release: 保证此操作之前的读写不会被重排到操作之后。用于释放锁。
  4. AcqRel: 同时包含 Acquire 和 Release。用于读-改-写操作。
  5. SeqCst: 顺序一致性。最严格,所有线程看到的操作顺序一致。

简单指南:

  • 计数器 → Relaxed
  • 简单的标志位同步 → Acquire / Release
  • 不确定用什么 → SeqCst (最安全,性能稍差)

4. 常见操作

  • load(Ordering): 读取值。
  • store(val, Ordering): 写入值。
  • fetch_add(val, Ordering): 原子地增加。
  • fetch_sub(val, Ordering): 原子地减少。
  • compare_exchange(current, new, success, failure): CAS 操作,无锁编程的核心。

初学者常见困惑

💡 这是很多学习者第一次遇到原子类型时的困惑——你并不孤单!

困惑 1: "既然原子类型这么快,为什么不用它替代 Mutex?"

解答: 原子类型只能保证单个值的原子性。如果你需要保护复杂的数据结构(如 HashMap),必须用 Mutex。

#![allow(unused)]
fn main() {
// ❌ 错误:无法用原子类型保护 HashMap
let map: Atomic<HashMap<String, i32>> ... // 不存在这种类型

// ✅ 正确:使用 Mutex
let map: Mutex<HashMap<String, i32>> = Mutex::new(HashMap::new());
}

困惑 2: "内存序到底是什么?"

解答: 现代 CPU 会重排指令以优化性能。内存序告诉 CPU 哪些重排是允许的。

  • Relaxed: 随便排,只要操作本身是原子的。
  • SeqCst: 别乱排,按代码顺序来。

常见错误

错误 1: 误用 Relaxed 导致同步失败

// 线程 1
data_ready.store(true, Ordering::Relaxed); // ❌ 其他线程可能还没看到 data 的更新

// 线程 2
if data_ready.load(Ordering::Relaxed) {
    // 可能读到旧数据!
}

修复方法: 使用 Release / Acquire

#![allow(unused)]
fn main() {
// 线程 1
data.store(42, Ordering::Relaxed);
data_ready.store(true, Ordering::Release); // ✅ 保证 data 的更新在 flag 之前可见

// 线程 2
if data_ready.load(Ordering::Acquire) {
    // 一定能读到 42
}
}

动手练习

练习 1: 实现线程安全的计数器

使用 AtomicUsize 实现一个计数器,10 个线程各增加 1000 次。

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    let counter = Arc::new(AtomicUsize::new(0));
    // TODO: 创建 10 个线程,每个线程循环 1000 次增加 counter
    // TODO: 等待所有线程结束
    // TODO: 打印最终结果 (应该是 10000)
}
点击查看答案
#![allow(unused)]
fn main() {
let mut handles = vec![];
for _ in 0..10 {
    let c = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        for _ in 0..1000 {
            c.fetch_add(1, Ordering::Relaxed);
        }
    }));
}
for h in handles { h.join().unwrap(); }
println!("Result: {}", counter.load(Ordering::Relaxed));
}

小结

核心要点:

  1. 原子类型: 硬件级原子操作,无锁。
  2. 适用场景: 简单状态、计数器、标志位。
  3. 内存序: Relaxed (计数器), Acquire/Release (同步), SeqCst (默认)。
  4. 对比 Mutex: 原子类型快但只能保护单个值;Mutex 慢但能保护复杂结构。

关键术语:

  • Atomic Operation (原子操作): 不可中断的操作。
  • Memory Ordering (内存序): 定义操作顺序的规则。
  • CAS (Compare-And-Swap): 比较并交换,无锁编程基础。
  • Lock-free (无锁): 不阻塞线程的并发算法。

术语表

English中文
Atomic Type原子类型
Memory Ordering内存序
Compare-And-Swap (CAS)比较并交换
Lock-free无锁
Sequential Consistency顺序一致性

延伸阅读

继续学习


知识检查

问题 1 🟢 (基础)

AtomicUsize 主要适用于什么场景?

A) 保护复杂的 HashMap
B) 线程安全的计数器或标志位
C) 替代所有的 Mutex
D) 存储大对象

点击查看答案

答案: B) 线程安全的计数器或标志位

问题 2 🟡 (中等)

以下哪种内存序最严格?

A) Relaxed
B) Acquire
C) Release
D) SeqCst

点击查看答案

答案: D) SeqCst (顺序一致性)

测试基础

开篇故事

想象你在建造一座大桥。你不会等到桥建好了才测试它是否稳固——你会在每一步都进行检查:地基是否牢固?钢筋强度够吗?混凝土配比正确吗?软件测试也是如此。测试不是最后才做的事情,而是贯穿整个开发过程的质量保障。

Rust 的测试系统就像一位严格的质检员——它在编译时就确保你的代码符合预期,让 bug 无处藏身。


本章适合谁

如果你想学习如何编写可靠的 Rust 代码,或者理解测试在 Rust 中的最佳实践,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 理解 Rust 测试的三种类型(单元、集成、文档)
  2. 使用 #[cfg(test)] 组织测试模块
  3. 使用 assert!assert_eq!assert_ne!
  4. 编写会 panic 的测试 (#[should_panic])
  5. 使用 #[ignore] 跳过慢测试
  6. 运行特定测试和并行测试

前置要求


第一个例子

最简单的测试:

#![allow(unused)]
fn main() {
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
}
}

运行测试:

cargo test

发生了什么?

  • #[cfg(test)] - 只在测试时编译
  • #[test] - 标记测试函数
  • assert_eq! - 断言相等

原理解析

1. 测试的三种类型

测试类型
├── 单元测试 (Unit Tests)
│   ├── 测试单个函数/模块
│   ├── 放在 src/ 文件中
│   └── 使用 #[cfg(test)]
├── 集成测试 (Integration Tests)
│   ├── 测试公共 API
│   ├── 放在 tests/ 目录
│   └── 像外部用户使用
└── 文档测试 (Doc Tests)
    ├── 测试文档示例
    ├── 放在 /// 注释中
    └── cargo test 自动运行

2. 单元测试组织

#![allow(unused)]
fn main() {
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn test_subtract() {
        assert_eq!(subtract(5, 3), 2);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, -1), -2);
    }
}
}

3. Assert 宏家族

#![allow(unused)]
fn main() {
#[test]
fn test_assertions() {
    // assert! - 条件必须为 true
    assert!(true);
    assert!(2 + 2 == 4);

    // assert_eq! - 两个值相等
    assert_eq!(4, 2 + 2);
    assert_eq!("hello", "hello");

    // assert_ne! - 两个值不相等
    assert_ne!(4, 5);
    assert_ne!("hello", "world");

    // 自定义错误消息
    assert!(2 + 2 == 4, "数学出错了!");
    assert_eq!(4, 2 + 2, "加法应该工作");
}
}

4. 应该 Panic 的测试

#![allow(unused)]
fn main() {
pub fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("除数不能为 0");
    }
    a / b
}

#[test]
#[should_panic(expected = "除数不能为 0")]
fn test_divide_by_zero() {
    divide(10, 0);
}
}

5. 忽略慢测试

#![allow(unused)]
fn main() {
#[test]
fn test_fast() {
    assert_eq!(1 + 1, 2);
}

#[test]
#[ignore]
fn test_slow() {
    // 这个测试很慢,默认跳过
    std::thread::sleep(std::time::Duration::from_secs(10));
    assert!(true);
}
}

运行被忽略的测试:

cargo test -- --ignored

6. 测试结果类型

#![allow(unused)]
fn main() {
#[test]
fn test_result() -> Result<(), String> {
    if 2 + 2 == 4 {
        Ok(())
    } else {
        Err(String::from("数学出错了"))
    }
}
}

7. 使用 nextest 批量测试

nextest 是 Rust 的下一代测试运行器,比 cargo test 更快、更强大。

安装:

cargo install cargo-nextest

基本使用:

# 运行所有测试
cargo nextest run

# 运行特定测试
cargo nextest run test_add

# 显示测试输出
cargo nextest run --nocapture

# 并行运行(默认使用所有 CPU 核心)
cargo nextest run --test-threads 4

nextest vs cargo test 对比:

特性cargo testcargo nextest
执行方式单进程每测试一进程
并行度有限完全并行
失败隔离差(一个失败影响其他)好(完全隔离)
重试支持支持 --retries
进度显示简单详细进度条
速度较慢快 2-5 倍

高级功能:

# 重试失败的测试
cargo nextest run --retries 2

# 只运行失败的测试
cargo nextest run --no-run  # 先记录
cargo nextest run --rerun   # 重跑失败

# 生成 JUnit 报告
cargo nextest run --message-format junit > report.xml

# 按特性过滤
cargo nextest run --features "feature1,feature2"

# 跳过特定测试
cargo nextest run --filter-expr "not test(/slow/)"

在 CI/CD 中使用:

# GitHub Actions 示例
- name: Install nextest
  run: cargo install cargo-nextest --locked

- name: Run tests
  run: cargo nextest run --retries 2

为什么选择 nextest?

  • 测试隔离:每个测试在独立进程中运行
  • 快速失败:立即显示失败信息
  • 更好的输出:彩色输出、进度条、详细统计
  • CI 友好:原生支持重试和报告生成

8. 工程实践:测试组织

测试目录结构

my-crate/
├── src/
│   ├── lib.rs
│   ├── module_a.rs
│   │   └── #[cfg(test)] mod tests { ... }  # 单元测试
│   └── module_b.rs
│       └── #[cfg(test)] mod tests { ... }  # 单元测试
├── tests/
│   ├── integration_test_a.rs                # 集成测试
│   └── integration_test_b.rs                # 集成测试
└── benches/
    └── benchmark_a.rs                       # 基准测试

测试辅助函数

创建共享的测试辅助函数:

#![allow(unused)]
fn main() {
// tests/common/mod.rs
pub fn setup_test_db() -> Database {
    // 创建测试数据库
}

pub fn cleanup_test_db(db: &Database) {
    // 清理测试数据
}

// tests/integration_test.rs
mod common;

#[test]
fn test_user_creation() {
    let db = common::setup_test_db();
    // 测试逻辑
    common::cleanup_test_db(&db);
}
}

属性测试 (Property-Based Testing)

使用 proptest crate 进行属性测试:

#![allow(unused)]
fn main() {
use proptest::prelude::*;

proptest! {
    #[test]
    fn test_sort_always_produces_sorted_vec(
        input in prop::collection::vec(any::<i32>(), 0..100)
    ) {
        let mut sorted = input.clone();
        sorted.sort();
        
        // 验证排序后的向量是有序的
        for i in 0..sorted.len().saturating_sub(1) {
            prop_assert!(sorted[i] <= sorted[i + 1]);
        }
        
        // 验证长度不变
        prop_assert_eq!(sorted.len(), input.len());
    }
}
}

Mock 测试

使用 mockall crate 创建 Mock 对象:

#![allow(unused)]
fn main() {
use mockall::automock;

#[automock]
trait Database {
    fn get_user(&self, id: u64) -> Option<User>;
    fn save_user(&self, user: &User) -> Result<(), Error>;
}

#[test]
fn test_user_service() {
    let mut mock_db = MockDatabase::new();
    mock_db.expect_get_user()
        .with(eq(1))
        .returning(|_| Some(User { id: 1, name: "Alice".into() }));
    
    let service = UserService::new(mock_db);
    let user = service.get_user(1);
    assert_eq!(user.unwrap().name, "Alice");
}
}

常见错误

错误 1: 忘记 #[cfg(test)]

#![allow(unused)]
fn main() {
// ❌ 错误:测试代码会被编译到生产代码中
mod tests {
    #[test]
    fn test_something() {}
}

// ✅ 正确:只在测试时编译
#[cfg(test)]
mod tests {
    #[test]
    fn test_something() {}
}
}

错误 2: 测试依赖外部状态

#![allow(unused)]
fn main() {
// ❌ 错误:依赖文件系统
#[test]
fn test_read_file() {
    let content = std::fs::read_to_string("data.txt").unwrap();
    assert_eq!(content, "expected");
}

// ✅ 正确:使用临时文件或 mock
#[test]
fn test_read_file() {
    let temp_dir = std::env::temp_dir();
    let file_path = temp_dir.join("test_data.txt");
    std::fs::write(&file_path, "expected").unwrap();
    // 测试完成后自动清理
}
}

动手练习

练习 1: 编写测试

为以下函数编写完整的测试:

#![allow(unused)]
fn main() {
pub fn is_even(n: i32) -> bool {
    n % 2 == 0
}

// TODO: 编写测试覆盖:
// - 正偶数
// - 正奇数
// - 零
// - 负偶数
// - 负奇数
}
点击查看答案
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_positive_even() {
        assert!(is_even(2));
        assert!(is_even(100));
    }

    #[test]
    fn test_positive_odd() {
        assert!(!is_even(1));
        assert!(!is_even(99));
    }

    #[test]
    fn test_zero() {
        assert!(is_even(0));
    }

    #[test]
    fn test_negative_even() {
        assert!(is_even(-2));
        assert!(is_even(-100));
    }

    #[test]
    fn test_negative_odd() {
        assert!(!is_even(-1));
        assert!(!is_even(-99));
    }
}
}

故障排查

Q: 如何运行单个测试?

A: cargo test test_name

Q: 如何并行运行测试?

A: cargo test -- --test-threads=4

Q: 如何显示测试输出?

A: cargo test -- --nocapture


小结

核心要点

  1. #[cfg(test)]: 只在测试时编译
  2. #[test]: 标记测试函数
  3. Assert 宏: 验证预期结果
  4. should_panic: 测试错误处理
  5. ignore: 跳过慢测试

术语表

English中文
Unit Test单元测试
Integration Test集成测试
Doc Test文档测试
Assertion断言
Panic恐慌
Test Fixture测试夹具

完整示例:src/advance/testing/test_sample.rs


知识检查

快速测验(答案在下方):

  1. #[cfg(test)] 的作用是什么?

  2. assert!assert_eq!assert_ne! 的区别?

  3. 如何测试会 panic 的函数?

点击查看答案与解析
  1. 只在测试编译时包含代码
  2. assert! = 条件为真,assert_eq! = 相等,assert_ne! = 不相等
  3. 使用 #[should_panic] 属性

关键理解: 测试是代码质量的重要保障。

继续学习

💡 记住:好的测试是代码最好的文档!

Mock 模拟测试

开篇故事

想象你要测试一个依赖数据库的服务。传统方式是:连接真实数据库 → 插入测试数据 → 测试 → 清理数据。Mock 就像是假数据库——它模拟数据库的行为,但不需要真实连接。mockall 库帮你轻松创建这些"假"对象。


本章适合谁

如果你需要编写单元测试(测试依赖外部服务、数据库、API),本章适合你。Mock 是单元测试的关键技术。


你会学到什么

完成本章后,你可以:

  1. 理解 Mock 测试概念
  2. 使用 mockall 创建 Mock 对象
  3. 模拟 trait 实现
  4. 设置期望和返回值
  5. 验证方法调用

前置要求

  • 测试基础 - 测试基础
  • 特征 - trait 基础
  • Arc 智能指针 - Arc 基础

依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add mockall --dev
cargo add async-trait

第一个例子

最简单的 Mock 使用:

use mockall::automock;
use std::sync::Arc;

// 定义 trait
#[automock]
trait HmsMonitorService {
    fn monitor(&self) -> bool;
}

// 使用 trait 的结构体
#[derive(Clone)]
pub struct MonitorMessageConsumerListener {
    monitor_service: Arc<dyn HmsMonitorService>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_monitor() {
        // 创建 Mock 对象
        let mut mock = MockHmsMonitorService::new();
        
        // 设置期望
        mock.expect_monitor()
            .returning(|| true);
        
        let listener = MonitorMessageConsumerListener {
            monitor_service: Arc::new(mock),
        };
        
        // 测试
        assert!(listener.monitor_service.monitor());
    }
}

完整示例: mock_sample.rs


原理解析

mockall 特性

mockall 是 Mock 测试库

  • ✅ 自动生成 Mock
  • ✅ 支持 trait
  • ✅ 期望验证
  • ✅ 返回值设置

使用 automock

使用 #[automock] 属性

use mockall::automock;

#[automock]
trait Database {
    fn connect(&self, url: &str) -> bool;
    fn query(&self, sql: &str) -> Vec<String>;
}

生成的 Mock 类型

  • MockDatabase: Mock 实现
  • expect_connect(): 设置期望
  • expect_query(): 设置期望

创建 Mock 对象

使用 new()

let mut mock = MockDatabase::new();

设置期望

使用 expect_*() 方法

mock.expect_connect()
    .with(eq("postgres://localhost"))  // 参数匹配
    .returning(|_| true);               // 返回值

mock.expect_query()
    .with(eq("SELECT *"))
    .returning(|_| vec!["row1".to_string()]);

验证调用

使用 times()

mock.expect_monitor()
    .times(1)      // 期望调用 1 次
    .returning(|| true);

验证调用顺序

let ctx = Mock::new_context();
mock.expect_connect().returning(|_| true);
mock.expect_query().returning(|_| vec![]);

// 按顺序调用
mock.connect("url");
mock.query("SELECT");

异步 Mock

使用 #[async_trait]

use async_trait::async_trait;
use mockall::automock;

#[automock]
#[async_trait]
trait AsyncService {
    async fn process(&self) -> bool;
}

#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn test_async() {
        let mut mock = MockAsyncService::new();
        mock.expect_process()
            .returning(|| async { true }.boxed());
        
        assert!(mock.process().await);
    }
}

常见错误

错误 1: 忘记设置返回值

mock.expect_monitor();  // ❌ 没有设置返回值
mock.monitor();         // ❌ 会 panic

错误信息:

MockHmsMonitorService::monitor: No matching expectation found

修复方法:

mock.expect_monitor()
    .returning(|| true);  // ✅ 设置返回值

错误 2: 参数不匹配

mock.expect_connect()
    .with(eq("postgres://localhost"));

mock.connect("mysql://localhost");  // ❌ 参数不匹配

错误信息:

MockDatabase::connect: No matching expectation found

修复方法:

mock.connect("postgres://localhost");  // ✅ 匹配期望

错误 3: 调用次数不匹配

mock.expect_monitor()
    .times(1);  // 期望 1 次

mock.monitor();
mock.monitor();  // ❌ 调用了 2 次

错误信息:

MockHmsMonitorService::monitor: Expectation called too many times

修复方法:

mock.expect_monitor()
    .times(2);  // ✅ 期望 2 次

动手练习

练习 1: 创建简单 Mock

use mockall::automock;

#[automock]
trait Calculator {
    fn add(&self, a: i32, b: i32) -> i32;
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_add() {
        // TODO: 创建 Mock
        // TODO: 设置期望
        // TODO: 验证结果
    }
}
点击查看答案
let mut mock = MockCalculator::new();
mock.expect_add()
    .returning(|a, b| a + b);

assert_eq!(mock.add(2, 3), 5);

练习 2: 验证调用次数

#[automock]
trait Logger {
    fn log(&self, msg: &str);
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_log_called() {
        // TODO: 创建 Mock
        // TODO: 期望调用 2 次
        // TODO: 调用并验证
    }
}
点击查看答案
let mut mock = MockLogger::new();
mock.expect_log()
    .times(2)
    .returning(|_| ());

mock.log("msg1");
mock.log("msg2");

练习 3: 参数匹配

#[automock]
trait UserService {
    fn get_user(&self, id: u32) -> String;
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_get_user() {
        // TODO: 创建 Mock
        // TODO: 设置不同 ID 的返回值
        // TODO: 验证结果
    }
}
点击查看答案
let mut mock = MockUserService::new();
mock.expect_get_user()
    .with(eq(1))
    .returning(|_| "Alice".to_string());
mock.expect_get_user()
    .with(eq(2))
    .returning(|_| "Bob".to_string());

assert_eq!(mock.get_user(1), "Alice");
assert_eq!(mock.get_user(2), "Bob");

故障排查 (FAQ)

Q: Mock 和真实实现有什么区别?

A:

  • 真实实现: 实际执行业务逻辑
  • Mock: 模拟行为,用于测试
  • 用途: Mock 用于单元测试,隔离依赖

Q: 什么时候使用 Mock?

A:

  • 测试依赖外部服务(数据库、API)
  • 测试边界情况(错误、超时)
  • 加速测试(避免真实 IO)

Q: Mock 会影响性能吗?

A:

  • Mock 本身性能开销很小
  • 主要用于测试,不影响生产性能
  • 测试速度通常更快(无真实 IO)

知识扩展

匹配器

use mockall::predicate::*;

mock.expect_query()
    .with(eq("SELECT *"))      // 精确匹配
    .returning(|_| vec![]);

mock.expect_query()
    .with(str::starts_with("SELECT"))  // 前缀匹配
    .returning(|_| vec![]);

返回 Result

mock.expect_connect()
    .returning(|_| Ok(()));

mock.expect_connect()
    .returning(|_| Err("Connection failed"));

多次调用不同返回值

mock.expect_monitor()
    .returning_st(|| {
        static mut CALLED: u32 = 0;
        unsafe {
            CALLED += 1;
            CALLED <= 2  // 前 2 次返回 true,之后 false
        }
    });

小结

核心要点

  1. mockall: 自动生成 Mock
  2. #[automock]: 为 trait 生成 Mock
  3. expect_*(): 设置期望
  4. returning(): 设置返回值
  5. times(): 验证调用次数

关键术语

  • Mock: 模拟对象
  • Expectation: 期望
  • Predicate: 谓词匹配
  • Stub: 桩实现

术语表

English中文
Mock模拟对象
Expectation期望
Predicate谓词
Stub
Trait特征

知识检查

快速测验(答案在下方):

  1. #[automock] 属性做了什么?

  2. 如何设置 Mock 的返回值?

  3. Mock 和真实实现的区别?

点击查看答案与解析
  1. 自动生成 trait 的 Mock 实现(MockTraitName
  2. mock.expect_method().returning(|args| value)
  3. Mock 是测试用的假实现,可控行为;真实实现是生产用的

关键理解: Mock 让你隔离测试,不依赖外部系统。

继续学习

前一章: Cow 类型
下一章: 测试框架

相关章节:

返回: 高级进阶


完整示例: mock_sample.rs

RSpec 测试框架

开篇故事

想象你要写测试报告。传统方式是:写测试函数 → 断言 → 打印结果。RSpec 就像是:用自然语言描述测试——"describe 用户服务,it 应该创建用户,it 应该删除用户"。rspec crate 帮你用 BDD 风格写测试。


本章适合谁

如果你想用 BDD 风格编写测试(行为驱动开发),本章适合你。RSpec 让测试更像文档。


你会学到什么

完成本章后,你可以:

  1. 理解 BDD 测试概念
  2. 使用 rspec crate
  3. 使用 speculate 宏
  4. 编写描述性测试
  5. 组织测试套件

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add rspec --dev
cargo add speculate --dev
cargo add mockall --dev

第一个例子

最简单的 RSpec 使用:

use rspec::describe;
use rspec::suite::Suite;

#[test]
fn test_rspec_suite() {
    rspec::run(&describe(
        "monitor_listener",
        (),  // 测试上下文
        |ctx| {
            ctx.it("should call monitor service", |_| {
                // 测试代码
                assert!(true);
            });
            
            ctx.it("should verify monitor call", |_| {
                assert_eq!(1 + 1, 2);
            });
        },
    ));
}

完整示例: rspec_sample.rs


原理解析

rspec 特性

rspec 是 BDD 测试框架

  • ✅ 描述性测试
  • ✅ 嵌套结构
  • ✅ 共享上下文
  • ✅ 行为驱动

使用 describe

使用 describe 组织测试

use rspec::describe;

describe("UserService", |ctx| {
    ctx.it("should create user", |_| {
        // 测试创建用户
    });
    
    ctx.it("should delete user", |_| {
        // 测试删除用户
    });
});

使用 speculate 宏

使用 speculate! 宏

use speculate::speculate;

speculate! {
    describe "UserService" {
        before {
            // 每个测试前执行
            let service = UserService::new();
        }
        
        it "should create user" {
            assert!(service.create("Alice"));
        }
        
        it "should delete user" {
            assert!(service.delete("Alice"));
        }
    }
}

共享上下文

使用 before 块

speculate! {
    describe "Database" {
        before {
            let db = Database::connect("test.db");
        }
        
        it "should connect" {
            assert!(db.is_connected());
        }
        
        it "should query" {
            let results = db.query("SELECT *");
            assert!(!results.is_empty());
        }
    }
}

嵌套描述

嵌套 describe

speculate! {
    describe "UserService" {
        describe "create" {
            it "should create valid user" {
                // ...
            }
            
            it "should reject invalid email" {
                // ...
            }
        }
        
        describe "delete" {
            it "should delete existing user" {
                // ...
            }
        }
    }
}

常见错误

错误 1: 忘记导入

speculate! {
    describe "Test" {
        it "should work" {
            assert!(true);
        }
    }
}
// ❌ 忘记 use speculate::speculate;

错误信息:

cannot find macro `speculate`

修复方法:

use speculate::speculate;  // ✅ 导入宏

错误 2: 语法错误

speculate! {
    describe "Test" {
        it "should work" {
            assert!(true);
        // ❌ 忘记闭合花括号

错误信息:

unexpected end of macro invocation

修复方法:

speculate! {
    describe "Test" {
        it "should work" {
            assert!(true);
        }
    }
}  // ✅ 闭合所有括号

错误 3: 共享变量作用域

speculate! {
    describe "Test" {
        let shared = 42;  // ❌ 变量作用域错误
        
        it "test1" {
            println!("{}", shared);
        }
    }
}

修复方法:

speculate! {
    describe "Test" {
        before {
            let shared = 42;  // ✅ 在 before 块中定义
        }
        
        it "test1" {
            println!("{}", shared);
        }
    }
}

动手练习

练习 1: 简单测试

use speculate::speculate;

speculate! {
    describe "Calculator" {
        // TODO: 测试加法
        // TODO: 测试减法
    }
}
点击查看答案
speculate! {
    describe "Calculator" {
        it "should add" {
            assert_eq!(2 + 2, 4);
        }
        
        it "should subtract" {
            assert_eq!(5 - 3, 2);
        }
    }
}

练习 2: 使用 before 块

speculate! {
    describe "UserService" {
        // TODO: 在 before 中创建服务
        // TODO: 测试创建用户
        // TODO: 测试查询用户
    }
}
点击查看答案
speculate! {
    describe "UserService" {
        before {
            let service = UserService::new();
        }
        
        it "should create user" {
            assert!(service.create("Alice"));
        }
        
        it "should find user" {
            let user = service.find("Alice");
            assert!(user.is_some());
        }
    }
}

练习 3: 嵌套描述

speculate! {
    // TODO: 描述 "Database"
    //   TODO: 描述 "connect"
    //     TODO: 测试成功连接
    //   TODO: 描述 "query"
    //     TODO: 测试查询结果
}
点击查看答案
speculate! {
    describe "Database" {
        describe "connect" {
            it "should connect successfully" {
                assert!(db.connect().is_ok());
            }
        }
        
        describe "query" {
            it "should return results" {
                let results = db.query("SELECT *");
                assert!(!results.is_empty());
            }
        }
    }
}

故障排查 (FAQ)

Q: RSpec 和标准测试有什么区别?

A:

  • 标准测试: #[test] fn test_xxx()
  • RSpec: 描述性,BDD 风格
  • 优势: RSpec 更像文档,易读

Q: 什么时候使用 RSpec?

A:

  • 需要行为驱动测试
  • 测试需要清晰描述
  • 团队熟悉 BDD

Q: RSpec 会影响性能吗?

A:

  • 运行时性能无影响(仅测试代码)
  • 编译时间略长(宏展开)
  • 测试执行速度相同

知识扩展

自定义匹配器

speculate! {
    describe "Custom Matcher" {
        it "should be even" {
            let num = 4;
            assert!(num % 2 == 0, "expected even number");
        }
    }
}

跳过测试

speculate! {
    describe "Skipped" {
        xit "should skip this test" {
            // 这个测试会被跳过
        }
    }
}

条件测试

#[cfg(feature = "advanced")]
speculate! {
    describe "Advanced" {
        it "should work with feature" {
            // 只在启用 advanced 特性时运行
        }
    }
}

小结

核心要点

  1. rspec: BDD 测试框架
  2. describe: 组织测试
  3. it: 单个测试
  4. before: 共享上下文
  5. speculate!: 宏语法

关键术语

  • BDD: 行为驱动开发
  • Describe: 描述块
  • It: 测试用例
  • Before: 前置块

术语表

English中文
BDD行为驱动开发
Describe描述块
It测试用例
Before前置块
Speculate推测宏

知识检查

快速测验(答案在下方):

  1. RSpec 风格测试和普通 Rust 测试有什么区别?

  2. describeit 的作用是什么?

  3. 什么时候应该使用 BDD 风格测试?

点击查看答案与解析
  1. RSpec 使用 describe/it 语法,更接近自然语言
  2. describe = 测试组,it = 单个测试用例
  3. 复杂业务逻辑、行为驱动开发、团队协作

关键理解: BDD 测试更易读,但需要额外依赖。

继续学习

前一章: Mock 模拟
下一章: 派生宏

相关章节:

返回: 高级进阶


完整示例: rspec_sample.rs

派生宏

开篇故事

想象你要为结构体实现 getter 和 setter 方法。传统方式是:手动写每个方法 → 容易出错 → 代码重复。派生宏就像是:告诉编译器"帮我生成这些方法",它自动完成。getset crate 就是这样的工具。


本章适合谁

如果你想减少样板代码(getter、setter、builder),本章适合你。派生宏是 Rust 元编程的基础。


你会学到什么

完成本章后,你可以:

  1. 理解派生宏概念
  2. 使用 getset crate
  3. 自动生成 getter/setter
  4. 使用 derive_more
  5. 创建 Builder 模式

前置要求

  • 结构体 - 结构体基础
  • 宏编程 - 宏基础
  • 可变性 - mutability 基础

依赖安装

运行以下命令安装所需依赖:

cargo add getset

第一个例子

最简单的 getset 使用:

use getset::{Getters, Setters};

#[derive(Getters, Setters)]
pub struct Category {
    #[getset(get = "pub", set = "pub")]
    id: u64,
    
    #[getset(get = "pub", set = "pub")]
    name: String,
}

fn main() {
    let mut cat = Category {
        id: 1,
        name: "Electronics".to_string(),
    };
    
    // 使用生成的 getter
    println!("ID: {}", cat.id());
    
    // 使用生成的 setter
    cat.set_name("Books".to_string());
}

完整示例: getset_sample.rs


原理解析

getset 特性

getset 是代码生成库

  • ✅ 自动生成 getter
  • ✅ 自动生成 setter
  • ✅ 减少样板代码
  • ✅ 类型安全

使用 Getters 派生

使用 #[derive(Getters)]

use getset::Getters;

#[derive(Getters)]
pub struct User {
    #[get = "pub"]
    id: u32,
    
    #[get = "pub"]
    name: String,
}

// 生成:
// impl User {
//     pub fn id(&self) -> &u32 { &self.id }
//     pub fn name(&self) -> &String { &self.name }
// }

使用 Setters 派生

使用 #[derive(Setters)]

use getset::Setters;

#[derive(Setters)]
pub struct User {
    #[set = "pub"]
    name: String,
}

// 生成:
// impl User {
//     pub fn set_name(&mut self, val: String) { self.name = val; }
// }

组合使用

使用 Getters + Setters

use getset::{Getters, Setters};

#[derive(Getters, Setters)]
pub struct Product {
    #[getset(get = "pub", set = "pub")]
    id: u64,
    
    #[getset(get = "pub", set = "pub")]
    name: String,
}

derive_more

使用 derive_more

use derive_more::Display;

#[derive(Display)]
#[display("User{{id:{}, name:{}}}", id, name)]
pub struct User {
    id: u32,
    name: String,
}

// 自动生成 Display 实现
let user = User { id: 1, name: "Alice".to_string() };
println!("{}", user);  // User{id:1, name:Alice}

Builder 模式

手动实现 Builder

pub struct Category {
    id: u64,
    name: String,
}

impl Category {
    pub fn builder() -> CategoryBuilder {
        CategoryBuilder::new()
    }
}

pub struct CategoryBuilder {
    inner: Category,
}

impl CategoryBuilder {
    pub fn new() -> Self {
        Self {
            inner: Category {
                id: 0,
                name: String::new(),
            },
        }
    }
    
    pub fn with_id(mut self, id: u64) -> Self {
        self.inner.id = id;
        self
    }
    
    pub fn with_name(mut self, name: String) -> Self {
        self.inner.name = name;
        self
    }
    
    pub fn build(self) -> Category {
        self.inner
    }
}

// 使用
let cat = Category::builder()
    .with_id(1)
    .with_name("Electronics".to_string())
    .build();

常见错误

错误 1: 忘记属性

use getset::Getters;

#[derive(Getters)]
pub struct User {
    id: u32,  // ❌ 忘记 #[get] 属性
}

错误信息:

no method named `id` found

修复方法:

#[derive(Getters)]
pub struct User {
    #[get = "pub"]
    id: u32,  // ✅ 添加属性
}

错误 2: 可见性错误

#[derive(Getters)]
pub struct User {
    #[get = "pub"]
    id: u32,
}

let user = User { id: 1 };
user.id();  // ✅ 可以访问

// 但在其他模块:
mod other {
    user.id();  // ❌ 如果 User 不是 pub 会失败
}

修复方法:

pub struct User {  // ✅ 结构体也必须是 pub
    #[get = "pub"]
    id: u32,
}

错误 3: Builder 模式错误

pub struct Builder {
    inner: User,
}

impl Builder {
    pub fn with_name(self, name: String) -> Self {
        self.inner.name = name;  // ❌ self 不是 mut
        self
    }
}

修复方法:

pub fn with_name(mut self, name: String) -> Self {
    // ✅ 添加 mut
    self.inner.name = name;
    self
}

动手练习

练习 1: 创建 Getters

use getset::Getters;

#[derive(Getters)]
pub struct Product {
    // TODO: 添加 id 字段 (u64)
    // TODO: 添加 name 字段 (String)
    // TODO: 添加 pub getter
}
点击查看答案
#[derive(Getters)]
pub struct Product {
    #[get = "pub"]
    id: u64,
    #[get = "pub"]
    name: String,
}

练习 2: 创建 Setters

use getset::Setters;

#[derive(Setters)]
pub struct User {
    // TODO: 添加 name 字段
    // TODO: 添加 pub setter
}
点击查看答案
#[derive(Setters)]
pub struct User {
    #[set = "pub"]
    name: String,
}

练习 3: 实现 Builder

pub struct Config {
    host: String,
    port: u16,
}

impl Config {
    // TODO: 实现 builder() 方法
}

pub struct ConfigBuilder {
    inner: Config,
}

impl ConfigBuilder {
    // TODO: 实现 new() 方法
    // TODO: 实现 with_host() 方法
    // TODO: 实现 with_port() 方法
    // TODO: 实现 build() 方法
}
点击查看答案
impl Config {
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::new()
    }
}

impl ConfigBuilder {
    pub fn new() -> Self {
        Self {
            inner: Config {
                host: String::new(),
                port: 0,
            },
        }
    }
    
    pub fn with_host(mut self, host: String) -> Self {
        self.inner.host = host;
        self
    }
    
    pub fn with_port(mut self, port: u16) -> Self {
        self.inner.port = port;
        self
    }
    
    pub fn build(self) -> Config {
        self.inner
    }
}

故障排查 (FAQ)

Q: getset 和手动实现有什么区别?

A:

  • getset: 自动生成,减少样板
  • 手动: 完全控制,更灵活
  • 推荐: getset 用于简单 getter/setter

Q: 什么时候使用 Builder 模式?

A:

  • 多个可选字段
  • 需要链式调用
  • 构建复杂对象

Q: derive_more 和 getset 有什么区别?

A:

  • derive_more: 派生各种 trait (Display, From, Into 等)
  • getset: 专门生成 getter/setter
  • 可以一起使用

知识扩展

可见性选项

#[derive(Getters)]
pub struct User {
    #[get = "pub"]      // 公开
    #[get = "pub(crate)"]  // crate 内公开
    #[get = "pub(super)"]  // 父模块公开
    #[get]              // 私有(默认)
    id: u32,
}

组合派生

use getset::{Getters, Setters, MutGetters};
use derive_more::Display;

#[derive(Getters, Setters, MutGetters, Display)]
#[display("User{{id:{}, name:{}}}", id, name)]
pub struct User {
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    id: u32,
    
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    name: String,
}

条件编译

#[cfg_attr(test, derive(Getters))]
pub struct Config {
    #[cfg_attr(test, get = "pub")]
    value: String,
}

小结

核心要点

  1. getset: 自动生成 getter/setter
  2. derive_more: 派生各种 trait
  3. Builder: 链式构建对象
  4. 可见性: 控制访问级别
  5. 减少样板: 提高开发效率

关键术语

  • Getter: 获取字段值
  • Setter: 设置字段值
  • Builder: 构建器模式
  • Derive: 派生宏

术语表

English中文
Getter获取方法
Setter设置方法
Builder构建器
Derive派生
Macro

知识检查

快速测验(答案在下方):

  1. #[derive(Getters)] 生成什么代码?

  2. #[get = "pub"]#[get] 有什么区别?

  3. 什么时候应该使用派生宏而不是手动实现?

点击查看答案与解析
  1. 为每个字段生成 fn field_name(&self) -> &Type 方法
  2. #[get = "pub"] 生成公共方法,#[get] 生成私有方法
  3. 字段多、样板代码多时使用派生宏,减少重复

关键理解: 派生宏是减少样板代码的有效工具。

延伸阅读

学习完派生宏后,你可能还想了解:

选择建议:

继续学习

前一章: RSpec 测试框架
下一章: 宏编程

相关章节:

返回: 高级进阶


完整示例: getset_sample.rs

宏编程

开篇故事

想象你经常写重复的代码。传统方式是:复制粘贴 → 修改 → 容易出错。宏就像是:告诉编译器"按这个模板生成代码",它自动完成。Rust 宏是强大的元编程工具。


本章适合谁

如果你想减少代码重复、创建 DSL(领域特定语言),本章适合你。宏是 Rust 元编程的基础。


你会学到什么

完成本章后,你可以:

  1. 理解宏的概念
  2. 使用声明宏 (macro_rules!)
  3. 使用过程宏 (proc-macro)
  4. 创建自定义宏
  5. 理解宏卫生

前置要求

  • 函数 - 函数基础
  • 特征 - trait 基础
  • 模块 - 模块基础

依赖安装

运行以下命令安装所需依赖:

cargo add macros --path crates/macros

第一个例子

最简单的宏使用:

// 声明宏
macro_rules! say_hello {
    () => {
        println!("Hello from macro!");
    };
}

fn main() {
    say_hello!();  // 调用宏
}

完整示例: macros_sample.rs


原理解析

宏展开过程

声明宏展开过程

源代码:
let v = make_vec!(1, 2, 3);
         │
         ▼
宏匹配阶段:
macro_rules! make_vec {
    ( $( $x:expr ),* ) => { ... }
}
         │
         ▼ 匹配: $(1, 2, 3)
         │
         ▼ 展开:
{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}
         │
         ▼
编译后代码:
let v = {
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
};

过程宏展开过程

源代码:
#[derive(Serialize, Deserialize)]
struct User { name: String }
         │
         ▼
编译时 (proc-macro):
1. 解析输入 TokenStream
2. 修改/生成 AST
3. 输出新 TokenStream
         │
         ▼
展开后代码:
struct User { name: String }
impl Serialize for User { ... }
impl Deserialize for User { ... }

宏的类型

Rust 有两种宏

  • 声明宏: macro_rules!
  • 过程宏: 自定义派生、属性、函数宏

声明宏

使用 macro_rules!

macro_rules! make_vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $( temp_vec.push($x); )*
            temp_vec
        }
    };
}

fn main() {
    let v = make_vec!(1, 2, 3, 4);
    println!("{:?}", v);  // [1, 2, 3, 4]
}

宏匹配模式

匹配不同参数

macro_rules! print_value {
    ( $val:expr ) => {
        println!("The value is: {}", $val);
    };
    
    ( $name:ident => $val:expr ) => {
        println!("{}: {}", stringify!($name), $val);
    };
}

fn main() {
    print_value!(42);
    print_value!(name => "Alice");
}

过程宏

使用过程宏

// 在 crates/macros/src/lib.rs
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn log(_attr: TokenStream, item: TokenStream) -> TokenStream {
    // 修改函数,添加日志
    // ...
}

// 使用
#[log]
fn say_hello(name: &str) {
    println!("Hello, {}!", name);
}

宏卫生

宏卫生 (Hygiene)

macro_rules! create_x {
    () => {
        let x = 42;
    };
}

fn main() {
    create_x!();
    // println!("{}", x);  // ❌ x 在宏作用域外不可见
}

初学者常见困惑

💡 这是很多学习者第一次遇到宏编程时的困惑——你并不孤单!

困惑 1: "宏和函数有什么区别?为什么需要宏?"

解答: 宏在编译时展开生成代码,函数在运行时执行:

函数:
fn add(a: i32, b: i32) -> i32 { a + b }
调用: add(1, 2) → 运行时执行 → 返回 3

宏:
macro_rules! add {
    ($a:expr, $b:expr) => { $a + $b };
}
调用: add!(1, 2) → 编译时展开为 1 + 2 → 编译后代码直接是 3

关键区别:

  • 函数: 运行时调用,有调用开销
  • : 编译时展开,零运行时开销,可以生成任意代码

为什么需要宏:

  • 减少重复代码(如 vec![1, 2, 3] 生成多个 push 调用)
  • 实现 DSL(领域特定语言,如 println!
  • 编译时计算和代码生成

困惑 2: "宏卫生 (Hygiene) 是什么?为什么宏里定义的变量外面不能用?"

解答: 宏卫生是 Rust 宏的安全特性——宏内部定义的标识符不会和外部冲突:

macro_rules! create_x {
    () => {
        let x = 42;  // 这个 x 在宏的"卫生作用域"内
    };
}

fn main() {
    create_x!();
    // println!("{}", x);  // ❌ 编译错误!x 不可见
}

为什么这样设计: 防止宏意外覆盖外部变量:

// 如果没有卫生机制:
macro_rules! bad_macro {
    () => { let x = 42; };
}

fn main() {
    let x = 10;
    bad_macro!();  // 如果宏能覆盖外部 x,这里 x 就变成 42 了!
    println!("{}", x);  // 预期 10,实际 42 — 严重 bug!
}

困惑 3: "什么时候用声明宏,什么时候用过程宏?"

解答:

场景推荐宏类型原因
简单代码生成声明宏简单、无需额外 crate
需要解析复杂语法过程宏可以访问 AST
自定义 #[derive]过程宏只能由过程宏实现
自定义属性 #[attr]过程宏只能由过程宏实现
需要外部 crate 依赖过程宏声明宏无法导入外部

简单判断:

  • 如果只是重复代码模式 → 声明宏
  • 如果需要修改结构体/枚举的定义 → 过程宏

困惑 4: "$() 语法到底是什么意思?"

解答: $() 是宏的重复模式,类似正则表达式的分组:

#![allow(unused)]
fn main() {
macro_rules! make_vec {
    ( $( $x:expr ),* ) => {
        // $()* 表示"重复零次或多次"
        {
            let mut temp_vec = Vec::new();
            $( temp_vec.push($x); )*  // 对每个 $x 执行一次
            temp_vec
        }
    };
}

// 调用:
make_vec!(1, 2, 3)
// 展开为:
{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}
}

常用重复模式:

  • $(...)* — 零次或多次
  • $(...)+ — 一次或多次
  • $(...)? — 零次或一次
  • 分隔符可以是 ,;、或任意符号

常见错误

错误 1: 宏作用域

mod utils {
    macro_rules! say_hello {
        () => { println!("Hello!"); };
    }
}

fn main() {
    say_hello!();  // ❌ 宏在模块外不可见
}

修复方法:

mod utils {
    #[macro_export]
    macro_rules! say_hello {
        () => { println!("Hello!"); };
    }
}

fn main() {
    say_hello!();  // ✅ 使用 #[macro_export]
}

错误 2: 参数不匹配

macro_rules! print_value {
    ( $val:expr ) => {
        println!("Value: {}", $val);
    };
}

print_value!(1, 2, 3);  // ❌ 期望 1 个参数,得到 3 个

修复方法:

macro_rules! print_values {
    ( $( $val:expr ),* ) => {
        $( println!("Value: {}", $val); )*
    };
}

print_values!(1, 2, 3);  // ✅ 多个参数

错误 3: 过程宏错误

#[log]  // ❌ 忘记导入宏
fn say_hello() {}

修复方法:

use macros::log;  // ✅ 导入宏

#[log]
fn say_hello() {}

动手练习

练习 1: 创建简单宏

// TODO: 创建 say_hi 宏
// 打印 "Hi from macro!"

fn main() {
    say_hi!();  // 调用宏
}
点击查看答案
macro_rules! say_hi {
    () => {
        println!("Hi from macro!");
    };
}

练习 2: 带参数的宏

// TODO: 创建 greet 宏
// 接受一个名字参数
// 打印 "Hello, {name}!"

fn main() {
    greet!("Alice");
}
点击查看答案
macro_rules! greet {
    ( $name:expr ) => {
        println!("Hello, {}!", $name);
    };
}

练习 3: 可变参数宏

// TODO: 创建 sum 宏
// 接受多个数字参数
// 打印总和

fn main() {
    sum!(1, 2, 3, 4, 5);  // 应该打印 15
}
点击查看答案
macro_rules! sum {
    ( $( $x:expr ),* ) => {
        let mut sum = 0;
        $( sum += $x; )*
        println!("Sum: {}", sum);
    };
}

故障排查 (FAQ)

Q: 宏和函数有什么区别?

A:

  • : 编译时展开,生成代码
  • 函数: 运行时调用,执行代码
  • : 更灵活,可以生成任意代码

Q: 什么时候使用宏?

A:

  • 减少代码重复
  • 创建 DSL
  • 元编程需求
  • 避免: 简单逻辑用函数

Q: 宏会影响编译时间吗?

A:

  • 会略微增加编译时间
  • 但通常可接受
  • 复杂宏影响更大

知识扩展

高级宏技巧

macro_rules! impl_trait_for_nums {
    ( $($t:ty),* ) => {
        $(
            impl MyTrait for $t {
                fn do_something(&self) {
                    println!("Doing for {}", self);
                }
            }
        )*
    };
}

impl_trait_for_nums!(i32, i64, u32, u64);

过程宏类型

// 属性宏
#[proc_macro_attribute]
pub fn my_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
    // ...
}

// 派生宏
#[proc_macro_derive(MyDerive)]
pub fn my_derive(item: TokenStream) -> TokenStream {
    // ...
}

// 函数宏
#[proc_macro]
pub fn my_macro(item: TokenStream) -> TokenStream {
    // ...
}

宏调试

macro_rules! debug {
    ( $val:expr ) => {
        println!("{} = {:?}", stringify!($val), $val);
    };
}

debug!(some_variable);
// 输出:some_variable = 42

小结

核心要点

  1. macro_rules!: 声明宏
  2. proc-macro: 过程宏
  3. 卫生: 宏作用域隔离
  4. 元编程: 编译时生成代码

关键术语

  • Macro: 宏
  • macro_rules!: 声明宏
  • proc-macro: 过程宏
  • Hygiene: 卫生
  • DSL: 领域特定语言

术语表

English中文
Macro
macro_rules!声明宏
Procedural Macro过程宏
Hygiene卫生
DSL领域特定语言

知识检查

快速测验(答案在下方):

  1. 声明宏和过程宏有什么区别?

  2. macro_rules! 中的 $() 语法是什么?

  3. 过程宏需要什么类型的 crate?

点击查看答案与解析
  1. 声明宏 = 模式匹配替换,过程宏 = Rust 代码操作 AST
  2. 重复匹配:$(...),* 匹配逗号分隔的零或多个项
  3. proc-macro = true 的 crate 类型

关键理解: 声明宏适合简单代码生成,过程宏适合复杂转换。

继续学习

前一章: 派生宏
下一章: 类型别名

相关章节:

返回: 高级进阶


完整示例: macros_sample.rs

类型别名

开篇故事

想象你经常写很长的类型名:Arc<RefCell<HashMap<String, Vec<User>>>>。每次都写很繁琐。类型别名就像是:给长类型起个短名字——type UserData = Arc<...>。这样代码更清晰易读。


本章适合谁

如果你想简化复杂类型、提高代码可读性,本章适合你。类型别名是 Rust 代码组织的基础。


你会学到什么

完成本章后,你可以:

  1. 理解类型别名概念
  2. 创建类型别名
  3. 简化复杂类型
  4. 使用类型别名组织代码
  5. 实现双向链表

前置要求

  • 智能指针 - Arc, Weak 基础
  • 泛型 - 泛型基础
  • 结构体 - 结构体基础

第一个例子

最简单的类型别名:

// 类型别名
type NodeCell = RefCell<TreeNode>;
type NodeArcPtr = Arc<NodeCell>;
type NodeWeakPtr = Weak<NodeCell>;

// 使用别名
fn create_node() -> NodeArcPtr {
    Arc::new(RefCell::new(TreeNode {
        value: 42,
        next: RefCell::new(None),
        prev: RefCell::new(None),
    }))
}

完整示例: typealias_sample.rs


原理解析

type 关键字

使用 type 创建别名

type Result<T> = std::result::Result<T, MyError>;
type Callback = Box<dyn Fn(i32) -> String>;

简化复杂类型

简化嵌套类型

// 不使用别名
fn process(data: Arc<RefCell<HashMap<String, Vec<User>>>>) {
    // ...
}

// 使用别名
type UserData = Arc<RefCell<HashMap<String, Vec<User>>>>;

fn process(data: UserData) {
    // 更清晰
}

双向链表应用

使用类型别名实现双向链表

type NodeCell = RefCell<TreeNode>;
type NodeArcPtr = Arc<NodeCell>;
type NodeWeakPtr = Weak<NodeCell>;

#[derive(Debug)]
struct TreeNode {
    value: i32,
    next: RefCell<Option<NodeArcPtr>>,
    prev: RefCell<Option<NodeWeakPtr>>,
}

impl TreeNode {
    fn new(value: i32) -> NodeArcPtr {
        Arc::new(RefCell::new(TreeNode {
            value,
            next: RefCell::new(None),
            prev: RefCell::new(None),
        }))
    }
    
    fn set_next(&self, next_node: Option<NodeArcPtr>) {
        self.next.borrow_mut().replace(next_node.unwrap());
    }
    
    fn get_next(&self) -> Option<NodeArcPtr> {
        self.next.borrow().clone()
    }
    
    fn set_prev(&self, prev_node: Option<&NodeArcPtr>) {
        let weak_ptr = prev_node.map(|arc| Arc::downgrade(arc));
        self.prev.borrow_mut().replace(weak_ptr.unwrap());
    }
    
    fn get_prev(&self) -> Option<NodeArcPtr> {
        self.prev
            .borrow()
            .as_ref()
            .and_then(|weak| weak.upgrade())
    }
}

类型别名优势

提高可读性

// 之前
fn connect(
    Arc<RefCell<dyn ConnectionHandler + Send + Sync>>
) -> Result<(), Box<dyn Error>>;

// 之后
type ConnectionPtr = Arc<RefCell<dyn ConnectionHandler + Send + Sync>>;

fn connect(conn: ConnectionPtr) -> Result<(), Box<dyn Error>>;

常见错误

错误 1: 循环依赖

type A = B;
type B = A;  // ❌ 循环定义

错误信息:

cycle detected when computing type of `B`

修复方法:

type A = i32;  // ✅ 打破循环
type B = A;

错误 2: 类型不匹配

type MyType = String;

let s: MyType = "hello";  // ❌ &str != String

修复方法:

let s: MyType = "hello".to_string();  // ✅ 转换为 String

错误 3: 生命周期错误

type NodePtr = Rc<RefCell<Node>>;

fn create_node() -> NodePtr {
    let node = Node::new();
    Rc::new(RefCell::new(node))  // ✅ 正确
}

动手练习

练习 1: 创建简单别名

// TODO: 创建 UserId 类型别名 (u64)
// TODO: 创建 UserName 类型别名 (String)

fn create_user(id: UserId, name: UserName) {
    println!("User {}: {}", id, name);
}
点击查看答案
type UserId = u64;
type UserName = String;

练习 2: 简化复杂类型

use std::sync::{Arc, Mutex};

// TODO: 创建 DataPtr 类型别名
// Arc<Mutex<Vec<String>>>

fn process_data(data: DataPtr) {
    let locked = data.lock().unwrap();
    println!("Data: {:?}", *locked);
}
点击查看答案
type DataPtr = Arc<Mutex<Vec<String>>>;

练习 3: 实现双向链表节点

use std::cell::RefCell;
use std::sync::{Arc, Weak};

// TODO: 定义类型别名
// NodeCell = RefCell<Node>
// NodePtr = Arc<NodeCell>
// NodeWeak = Weak<NodeCell>

struct Node {
    value: i32,
    next: RefCell<Option<NodePtr>>,
    prev: RefCell<Option<NodeWeak>>,
}
点击查看答案
type NodeCell = RefCell<Node>;
type NodePtr = Arc<NodeCell>;
type NodeWeak = Weak<NodeCell>;

故障排查 (FAQ)

Q: 类型别名和新类型有什么区别?

A:

  • 类型别名: type A = B,只是别名
  • 新类型: struct A(B),新类型,类型安全

Q: 什么时候使用类型别名?

A:

  • 简化复杂类型
  • 提高可读性
  • 统一类型使用

Q: 类型别名会影响性能吗?

A:

  • 不会,编译后完全一样
  • 零成本抽象

知识扩展

泛型别名

type Result<T> = std::result::Result<T, MyError>;
type Callback<T> = Box<dyn Fn(T) -> String>;

条件类型别名

#[cfg(target_os = "linux")]
type PlatformPath = std::path::UnixPath;

#[cfg(target_os = "windows")]
type PlatformPath = std::path::WindowsPath;

关联类型别名

trait Container {
    type Item;
    type Iter<'a>: Iterator<Item = Self::Item>
    where
        Self: 'a;
}

小结

核心要点

  1. type: 创建类型别名
  2. 简化: 简化复杂类型
  3. 可读性: 提高代码可读性
  4. 零成本: 无性能开销
  5. 组织: 组织代码结构

关键术语

  • Type Alias: 类型别名
  • Arc: 原子引用计数
  • Weak: 弱引用
  • RefCell: 运行时借用检查

术语表

English中文
Type Alias类型别名
Arc原子引用计数
Weak弱引用
RefCell引用单元
Bidirectional List双向链表

知识检查

快速测验(答案在下方):

  1. typenewtype 有什么区别?

  2. 类型别名会增加运行时开销吗?

  3. 什么时候应该使用类型别名?

点击查看答案与解析
  1. type 只是别名(编译时),newtype 是新类型(包装结构体)
  2. 不会 - 类型别名在编译时消除
  3. 简化复杂类型、提高可读性、减少重复

关键理解: 类型别名是零成本抽象。

继续学习

前一章: 宏编程
下一章: 对象存储

相关章节:

返回: 高级进阶


完整示例: typealias_sample.rs

对象存储

开篇故事

想象你要存储大量文件(图片、文档、备份)。传统方式是:买硬盘 → 管理文件系统 → 处理备份。对象存储就像是:云存储仓库——你只管存储和读取,其他都交给服务处理。


本章适合谁

如果你需要在 Rust 程序中存储和检索文件(本地或云存储),本章适合你。object_store 是统一的对象存储接口。


你会学到什么

完成本章后,你可以:

  1. 理解对象存储概念
  2. 使用 LocalFileSystem
  3. 存储键值对
  4. 检索对象数据
  5. 列出对象列表

前置要求

  • 异步编程 - async/await 基础
  • Tokio 运行时 - Tokio 基础
  • 错误处理 - 错误处理基础

依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add object_store
cargo add futures

第一个例子

最简单的对象存储使用:

use object_store::{ObjectStore, path::Path};
use object_store::local::LocalFileSystem;
use bytes::Bytes;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 获取临时目录
    let temp_dir = std::env::temp_dir().join("hello");
    
    // 创建本地文件系统存储
    let store = Arc::new(LocalFileSystem::new_with_prefix(temp_dir)?);
    
    // 存储数据
    let key = Path::from("my_key.txt");
    let value = Bytes::from("Hello, Object Store!");
    
    store.put(&key, value.clone().into()).await?;
    println!("存储成功!");
    
    // 检索数据
    let result = store.get(&key).await?;
    let bytes = result.bytes().await?;
    println!("检索到的值:{}", String::from_utf8_lossy(&bytes));
    
    Ok(())
}

完整示例: objectstore_sample.rs


原理解析

object_store 特性

object_store 是对象存储库

  • ✅ 统一接口
  • ✅ 支持多种后端
  • ✅ 异步支持
  • ✅ 类型安全

初始化存储

使用 LocalFileSystem

use object_store::local::LocalFileSystem;

// 创建本地存储
let store = LocalFileSystem::new_with_prefix("/tmp/storage")?;

使用内存存储

use object_store::memory::InMemory;

// 创建内存存储(用于测试)
let store = InMemory::new();

存储对象

使用 put()

use object_store::{ObjectStore, path::Path};
use bytes::Bytes;

let key = Path::from("data/file.txt");
let value = Bytes::from("Hello, World!");

store.put(&key, value.into()).await?;

检索对象

使用 get()

use object_store::ObjectStore;

let key = Path::from("data/file.txt");

// 获取对象
let result = store.get(&key).await?;

// 读取字节
let bytes = result.bytes().await?;
println!("{}", String::from_utf8_lossy(&bytes));

列出对象

使用 list()

use object_store::{ObjectStore, path::Path};
use futures::StreamExt;

let prefix = Path::from("data");

// 列出所有带前缀的对象
let mut stream = store.list(Some(&prefix));

while let Some(meta) = stream.next().await {
    println!("对象:{:?}", meta?.location);
}

常见错误

错误 1: 目录不存在

let store = LocalFileSystem::new_with_prefix("/nonexistent/path");
// ❌ 目录不存在

错误信息:

No such file or directory

修复方法:

// 先创建目录
tokio::fs::create_dir_all("/tmp/storage").await?;
let store = LocalFileSystem::new_with_prefix("/tmp/storage")?;

错误 2: 忘记 await

let result = store.get(&key);  // ❌ 忘记 .await
let bytes = result.bytes().await?;

错误信息:

no method named `bytes` on type `impl Future`

修复方法:

let result = store.get(&key).await?;  // ✅ 添加 .await

错误 3: 路径错误

let key = Path::from("wrong/path/file.txt");
let result = store.get(&key).await?;
// ❌ 文件不存在

错误信息:

Object not found

修复方法:

let key = Path::from("data/file.txt");  // ✅ 正确路径

动手练习

练习 1: 创建存储

use object_store::local::LocalFileSystem;

#[tokio::main]
async fn main() {
    // TODO: 创建临时目录
    // TODO: 创建 LocalFileSystem 实例
    // TODO: 打印存储路径
}
点击查看答案
let temp_dir = std::env::temp_dir().join("hello");
let store = LocalFileSystem::new_with_prefix(temp_dir).unwrap();
println!("存储路径:{:?}", store);

练习 2: 存储数据

use object_store::{ObjectStore, path::Path};
use bytes::Bytes;

#[tokio::main]
async fn main() {
    let store = /* 创建存储 */;
    
    // TODO: 创建键
    // TODO: 创建值
    // TODO: 存储数据
}
点击查看答案
let key = Path::from("test.txt");
let value = Bytes::from("Hello!");
store.put(&key, value.into()).await.unwrap();

练习 3: 检索数据

use object_store::ObjectStore;

#[tokio::main]
async fn main() {
    let store = /* 创建存储 */;
    
    // TODO: 获取对象
    // TODO: 读取字节
    // TODO: 打印内容
}
点击查看答案
let key = Path::from("test.txt");
let result = store.get(&key).await.unwrap();
let bytes = result.bytes().await.unwrap();
println!("{}", String::from_utf8_lossy(&bytes));

故障排查 (FAQ)

Q: object_store 支持哪些后端?

A:

  • LocalFileSystem(本地文件)
  • InMemory(内存)
  • AWS S3
  • Google Cloud Storage
  • Azure Blob Storage

Q: 如何切换到云存储?

A:

// AWS S3
use object_store::aws::AmazonS3Builder;

let store = AmazonS3Builder::new()
    .with_bucket_name("my-bucket")
    .with_region("us-east-1")
    .build()?;

Q: 如何处理大文件?

A:

// 流式上传
let mut writer = store.put_multipart(&path).await?;
writer.write(&chunk1).await?;
writer.write(&chunk2).await?;
writer.shutdown().await?;

知识扩展

删除对象

use object_store::ObjectStore;

store.delete(&Path::from("old_file.txt")).await?;

复制对象

use object_store::ObjectStore;

store.copy(
    &Path::from("source.txt"),
    &Path::from("dest.txt")
).await?;

元数据

use object_store::ObjectStore;

let meta = store.head(&Path::from("file.txt")).await?;
println!("大小:{} bytes", meta.size);
println!("最后修改:{}", meta.last_modified);

小结

核心要点

  1. ObjectStore: 统一接口
  2. LocalFileSystem: 本地存储
  3. put(): 存储对象
  4. get(): 检索对象
  5. list(): 列出对象

关键术语

  • Object Store: 对象存储
  • Path: 路径
  • Put: 存储
  • Get: 获取

术语表

English中文
Object Store对象存储
Path路径
Put存储
Get获取
List列出

知识检查

快速测验(答案在下方):

  1. 对象存储和文件系统有什么区别?

  2. 常见的对象存储服务有哪些?

  3. object_store crate 提供什么抽象?

点击查看答案与解析
  1. 对象存储是扁平的(key-value),文件系统是分层的(目录树)
  2. AWS S3, GCP Cloud Storage, Azure Blob Storage
  3. 统一的对象存储接口,支持多种后端

关键理解: 对象存储适合大规模数据存储和访问。

继续学习

前一章: 资源嵌入
下一章: Ollama AI 集成

相关章节:

  • 资源嵌入
  • Ollama AI 集成
  • 异步编程

返回: 高级进阶


完整示例: objectstore_sample.rs

服务框架

开篇故事

想象你在经营餐厅。单个厨师可以炒菜,但要有秩序地运营餐厅,你需要:前台接待、后厨管理、传菜员、收银员。服务框架就像餐厅管理系统——协调各个"服务"(组件)有序工作,处理顾客(请求)订单。

在 Rust 中,服务框架提供应用生命周期管理、依赖注入、错误处理等基础设施,让你专注于业务逻辑而非样板代码。


本章适合谁

如果你要构建中型到大型的 Rust 应用(Web 服务、微服务、后台进程),本章适合你。服务框架帮助你组织代码、管理依赖、处理错误。


你会学到什么

完成本章后,你可以:

  1. 理解服务框架的核心组件
  2. 实现依赖注入模式
  3. 管理服务生命周期
  4. 设计可测试的服务架构

前置要求

学习本章前,你需要理解:


依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add thiserror
cargo add async-trait

第一个例子

让我们看一个最简单的服务定义:

use async_trait::async_trait;

#[async_trait]
pub trait Service: Send + Sync {
    async fn start(&self) -> Result<(), Box<dyn std::error::Error>>;
    async fn stop(&self) -> Result<(), Box<dyn std::error::Error>>;
    async fn health_check(&self) -> bool;
}

pub struct MyService {
    name: String,
}

#[async_trait]
impl Service for MyService {
    async fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
        println!("启动服务:{}", self.name);
        Ok(())
    }

    async fn stop(&self) -> Result<(), Box<dyn std::error::Error>> {
        println!("停止服务:{}", self.name);
        Ok(())
    }

    async fn health_check(&self) -> bool {
        true
    }
}

发生了什么?

  • Service trait 定义服务接口
  • start() / stop(): 生命周期管理
  • health_check(): 健康检查

原理解析

1. 服务生命周期

pub enum ServiceState {
    Stopped,
    Starting,
    Running,
    Stopping,
}

pub struct ServiceManager {
    state: RwLock<ServiceState>,
    services: Vec<Box<dyn Service>>,
}

impl ServiceManager {
    pub async fn start_all(&self) -> Result<(), Error> {
        *self.state.write().await = ServiceState::Starting;
        
        for service in &self.services {
            service.start().await?;
        }
        
        *self.state.write().await = ServiceState::Running;
        Ok(())
    }
}

状态转换

Stopped → Starting → Running → Stopping → Stopped

2. 依赖注入模式

// 定义依赖 trait
#[async_trait]
pub trait Database: Send + Sync {
    async fn query(&self, sql: &str) -> Result<Vec<Row>>;
}

// 服务持有依赖
pub struct UserService<DB: Database> {
    db: DB,
}

impl<DB: Database> UserService<DB> {
    pub fn new(db: DB) -> Self {
        Self { db }
    }
    
    pub async fn get_user(&self, id: i32) -> Result<User> {
        self.db.query("SELECT * FROM users WHERE id = $1").await
    }
}

// 组合服务
pub struct App {
    user_service: UserService<PgDatabase>,
}

优势

  • 服务解耦
  • 易于测试(可注入 Mock)
  • 依赖显式声明

3. 健康检查模式

pub struct HealthStatus {
    pub service: String,
    pub healthy: bool,
    pub message: String,
}

#[async_trait]
pub trait HealthCheck: Send + Sync {
    async fn health(&self) -> HealthStatus;
}

impl HealthCheck for App {
    async fn health(&self) -> HealthStatus {
        let db_healthy = self.user_service.db.ping().await.is_ok();
        
        HealthStatus {
            service: "app".to_string(),
            healthy: db_healthy,
            message: if db_healthy { 
                "OK".to_string() 
            } else { 
                "Database connection failed".to_string() 
            },
        }
    }
}

4. 错误处理策略

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ServiceError {
    #[error("启动失败:{0}")]
    StartupFailed(#[from] std::io::Error),
    
    #[error("依赖缺失:{0}")]
    DependencyMissing(String),
    
    #[error("健康检查失败:{0}")]
    HealthCheckFailed(String),
}

#[async_trait]
impl Service for MyService {
    async fn start(&self) -> Result<(), ServiceError> {
        // 具体实现
        Ok(())
    }
}

常见错误

错误 1: 循环依赖

// ❌ 错误:A 依赖 B,B 依赖 A
struct ServiceA { b: ServiceB }
struct ServiceB { a: ServiceA }

// ✅ 正确:使用 trait 或事件解耦
trait EventHandler {
    fn handle(&self, event: Event);
}

struct ServiceA<E: EventHandler> { handler: E }
struct ServiceB { /* 不持有 ServiceA */ }

错误 2: 忘记清理资源

// ❌ 错误:stop() 为空
async fn stop(&self) -> Result<(), Error> {
    // 忘记关闭数据库连接
}

// ✅ 正确:清理所有资源
async fn stop(&self) -> Result<(), Error> {
    self.db.close().await?;
    self.cache.clear().await;
    Ok(())
}

错误 3: 阻塞异步服务

// ❌ 错误:在异步服务中同步阻塞
async fn process(&self) {
    std::thread::sleep(Duration::from_secs(1));  // 阻塞
}

// ✅ 正确:使用异步等待
async fn process(&self) {
    tokio::time::sleep(Duration::from_secs(1)).await;
}

动手练习

练习 1: 实现日志服务

创建服务框架:

#[async_trait]
pub trait Logger: Send + Sync {
    async fn log(&self, message: &str);
    async fn flush(&self);
}

// TODO: 实现 ConsoleLogger 和 FileLogger
点击查看答案
pub struct ConsoleLogger;

#[async_trait]
impl Logger for ConsoleLogger {
    async fn log(&self, message: &str) {
        println!("[LOG] {}", message);
    }
    
    async fn flush(&self) {
        // 控制台不需要 flush
    }
}

pub struct FileLogger {
    file: tokio::fs::File,
}

#[async_trait]
impl Logger for FileLogger {
    async fn log(&self, message: &str) {
        use tokio::io::AsyncWriteExt;
        let mut file = self.file.try_clone().await.unwrap();
        file.write_all(message.as_bytes()).await.unwrap();
    }
    
    async fn flush(&self) {
        // 刷新文件缓冲区
    }
}

解析: 不同日志实现可以互换,符合依赖注入原则。


故障排查

Q: 何时使用服务框架?

A: 当应用满足以下条件时:

  • 多个组件需要协调
  • 需要统一的生命周期管理
  • 依赖关系复杂
  • 需要可测试性

Q: 服务框架的性能开销?

A:

  • Trait 对象有轻微动态分发开销
  • 依赖注入增加间接层
  • 但对于业务逻辑,开销可忽略

Q: 如何测试服务?

A: 使用 Mock 实现:

struct MockDatabase;

#[async_trait]
impl Database for MockDatabase {
    async fn query(&self, _sql: &str) -> Result<Vec<Row>> {
        Ok(vec![])  // 返回测试数据
    }
}

小结

核心要点

  1. 服务接口: 定义统一的 start/stop/health 方法
  2. 依赖注入: 通过 trait 解耦服务
  3. 生命周期: 管理服务的启动和停止
  4. 健康检查: 提供服务状态监控

关键术语

  • Service: 服务
  • Dependency Injection: 依赖注入
  • Lifecycle: 生命周期
  • Health Check: 健康检查
  • Trait Object: 特征对象
  • Mock: 模拟

下一步


术语表

English中文
Service服务
Dependency依赖
Injection注入
Lifecycle生命周期
Health Check健康检查
Trait Object特征对象
Mock模拟

完整示例:src/advance/tools/services_sample.rs


知识检查

快速测验(答案在下方):

  1. 服务生命周期的三个阶段是什么?

  2. 依赖注入的作用是什么?

  3. 如何测试使用依赖注入的服务?

点击查看答案与解析
  1. 启动 (Start) → 运行 (Running) → 停止 (Stop)
  2. 解耦服务,使组件可替换、可测试
  3. 注入 Mock 依赖,验证服务行为

关键理解: 好的服务框架让复杂应用变得简单。

继续学习

💡 记住:好的服务框架让复杂应用变得简单。定义清晰的接口,注入依赖,管理生命周期!

阶段复习:高级进阶

开篇故事

想象你学完了所有驾驶技巧——高速公路、夜间驾驶、雨雪天气、紧急避让。现在你需要一次综合路考,把所有技能串联起来。阶段复习就是你的"高级路考"——把分散的高级知识整合成完整的生产能力。


本章适合谁

如果你已经完成了高级进阶部分(异步、数据库、Web、系统编程等),现在想检验自己的学习成果,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 综合运用异步编程、数据库、Web 框架知识
  2. 设计包含服务层、数据层、API 层的完整应用
  3. 识别和修复高级 Rust 编译错误
  4. 理解生产级 Rust 项目的架构模式

前置要求

完成以下章节:


第一个例子

回顾异步 + 数据库 + Web 的整合模式:

use axum::{Json, Router, routing::get};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct User {
    id: u64,
    name: String,
}

async fn get_users() -> Json<Vec<User>> {
    Json(vec![
        User { id: 1, name: "Alice".to_string() },
        User { id: 2, name: "Bob".to_string() },
    ])
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users", get(get_users));
    
    println!("Server running on http://localhost:3000");
    axum::serve(
        tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(),
        app
    ).await.unwrap();
}

这个例子整合了:异步运行时 (Tokio) + Web 框架 (Axum) + 序列化 (Serde)。


原理解析

高级知识整合图

异步编程 ──→ 数据库操作 ──→ Web 开发
    ↓            ↓            ↓
Tokio 运行时   SQLx/Diesel  Axum/Hyper
    ↓            ↓            ↓
    └────────────┴────────────┘
                 ↓
          生产级服务架构
                 ↓
    ┌────────────┴────────────┐
    ↓                         ↓
数据处理                  系统编程
(JSON/CSV/Rkyv)        (进程/文件/内存)
    ↓                         ↓
    └────────────┬────────────┘
                 ↓
          测试与模拟
        (Mock/RSpec)

每个模块都可以独立使用,但组合起来就是完整的生产应用。


复习范围

异步编程、数据库操作、Web 开发、数据处理、系统编程、测试与模拟、宏编程


综合练习:构建一个简单的 REST API 服务

练习 1:定义数据模型

// TODO: 定义 Task 结构体
// 字段:id (u64), title (String), completed (bool), created_at (String)
// 派生:Serialize, Deserialize, Debug, Clone

// TODO: 定义 TaskStore 结构体
// 字段:tasks (HashMap<u64, Task>), next_id (u64)
点击查看答案
use serde::{Serialize, Deserialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Task {
    id: u64,
    title: String,
    completed: bool,
    created_at: String,
}

#[derive(Debug)]
struct TaskStore {
    tasks: HashMap<u64, Task>,
    next_id: u64,
}

impl TaskStore {
    fn new() -> Self {
        TaskStore {
            tasks: HashMap::new(),
            next_id: 1,
        }
    }
}

练习 2:实现 CRUD 操作

// TODO: 实现 TaskStore 的方法
// - create_task(&mut self, title: String) -> Task
// - get_task(&self, id: u64) -> Option<&Task>
// - update_task(&mut self, id: u64, title: String) -> Result<Task, String>
// - delete_task(&mut self, id: u64) -> Result<Task, String>
// - list_tasks(&self) -> Vec<&Task>
点击查看答案
impl TaskStore {
    fn create_task(&mut self, title: String) -> Task {
        let task = Task {
            id: self.next_id,
            title,
            completed: false,
            created_at: chrono::Utc::now().to_rfc3339(),
        };
        self.tasks.insert(task.id, task.clone());
        self.next_id += 1;
        task
    }

    fn get_task(&self, id: u64) -> Option<&Task> {
        self.tasks.get(&id)
    }

    fn update_task(&mut self, id: u64, title: String) -> Result<Task, String> {
        let task = self.tasks.get_mut(&id)
            .ok_or_else(|| format!("Task {} not found", id))?;
        task.title = title;
        Ok(task.clone())
    }

    fn delete_task(&mut self, id: u64) -> Result<Task, String> {
        self.tasks.remove(&id)
            .ok_or_else(|| format!("Task {} not found", id))
    }

    fn list_tasks(&self) -> Vec<&Task> {
        self.tasks.values().collect()
    }
}

练习 3:异步任务处理

// TODO: 使用 tokio 实现异步任务保存
// - save_to_file(&self, path: &str) -> Result<(), Box<dyn Error>>
// - load_from_file(&mut self, path: &str) -> Result<(), Box<dyn Error>>
// 使用 tokio::fs 进行异步文件操作
点击查看答案
use tokio::fs;
use std::error::Error;

impl TaskStore {
    async fn save_to_file(&self, path: &str) -> Result<(), Box<dyn Error>> {
        let json = serde_json::to_string_pretty(&self.tasks)?;
        fs::write(path, json).await?;
        Ok(())
    }

    async fn load_from_file(&mut self, path: &str) -> Result<(), Box<dyn Error>> {
        let content = fs::read_to_string(path).await?;
        self.tasks = serde_json::from_str(&content)?;
        Ok(())
    }
}

知识检查

问题 1:Future 惰性

async fn fetch_data() -> String {
    println!("Fetching...");
    "data".to_string()
}

fn main() {
    let future = fetch_data();
    println!("Created future");
    // 没有 .await
}

会打印什么?

点击查看答案

只打印 "Created future"

Future 是惰性的,不调用 .awaitblock_on 就不会执行。fetch_data() 只是创建了 Future 对象,没有执行函数体。

问题 2:Arc vs Rc

为什么多线程中使用 Rc<T> 会编译失败?

点击查看答案

Rc<T> 不是线程安全的(引用计数操作不是原子的)。

多线程中应该使用 Arc<T>(Atomic Reference Counting),它的引用计数操作是原子的,保证线程安全。

问题 3:mpsc vs oneshot

何时使用 mpsc 通道,何时使用 oneshot 通道?

点击查看答案
  • mpsc: 多发送端,单接收端。用于持续通信(如任务间消息传递)。
  • oneshot: 单次通信。用于请求-响应模式(如等待一个结果)。

问题 4:宏卫生

macro_rules! create_var {
    () => {
        let x = 42;
    };
}

fn main() {
    create_var!();
    println!("{}", x);  // 能编译吗?
}
点击查看答案

不能编译。Rust 宏是卫生的(hygienic),宏内部创建的变量在宏外部不可见。

这是 Rust 宏的安全特性,防止宏意外覆盖外部变量。

问题 5:serde 自定义序列化

如何为字段自定义序列化行为?

点击查看答案
#[derive(Serialize, Deserialize)]
struct User {
    #[serde(rename = "user_name")]
    name: String,
    
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,
    
    #[serde(default = "default_age")]
    age: u8,
    
    #[serde(flatten)]
    extra: HashMap<String, Value>,
}

常见错误回顾

错误原因修复
future cannot be sent between threadsFuture 包含非 Send 类型使用 Send 安全的类型,检查闭包捕获
borrowed value does not live long enough生命周期不足添加生命周期标注或改变数据结构
cannot move out of shared reference试图移动借用的值使用 .clone() 或引用
no method named 'await'不在 async 上下文中#[tokio::main]async fn 中使用
expected struct, found enum类型不匹配检查 serde 派生和字段类型

小结

核心要点

  1. 异步编程是生产级 Rust 的基础
  2. 数据库 + Web 是最常见的应用组合
  3. 错误处理必须贯穿整个应用
  4. 测试保证代码质量
  5. 复习是巩固知识的关键

关键术语

  • Future: 异步计算
  • Executor: 执行器 (Tokio)
  • ORM: 对象关系映射
  • REST API: RESTful 接口
  • Mock: 模拟对象

术语表

English中文
Future异步计算
Executor执行器
ORM对象关系映射
REST APIRESTful 接口
Mock模拟对象
Serialization序列化
Deserialization反序列化

继续学习

💡 记住:复习是学习的重要部分。不要急于前进,确保每个概念都理解了!

💡 记住:高级概念需要更多实践。不要只看代码,动手写、运行、调试!

精选实战 (Awesome)

📖 学习内容概览

太棒了!你已经完成了 基础入门高级进阶!现在你已经掌握了 Rust 的核心概念和生态系统工具。在精选实战部分,你将学习如何构建生产级的应用程序,使用真实的框架和最佳实践。


🎯 你将学到什么

完成本部分学习后,你将能够:

  1. 构建生产服务 - 使用服务框架和生命周期管理
  2. 依赖注入 - 实现松耦合的模块化架构
  3. 消息队列集成 - 使用 MQTT 进行异步通信
  4. 模板引擎 - 使用 Tera、Liquid、Pest 生成动态内容
  5. 数据处理 - 使用 Polars 进行数据分析
  6. 插件系统 - 构建可扩展的插件架构

📚 章节列表

章节说明难度预计时间
服务框架服务生命周期、注册、管理🔴 困难90 分钟
依赖注入DI 模式、容器、特性🔴 困难90 分钟
数据库实战SurrealDB、SQLite 高级用法🟡 中等60 分钟
消息队列MQTT Broker 和 Client🟡 中等60 分钟
序列生成UUID、雪花算法🟡 中等45 分钟
模板引擎Tera、Liquid、Pest🟡 中等60 分钟
数据处理Polars 数据分析🟡 中等60 分钟
插件系统插件架构、动态加载🔴 困难90 分钟

🔗 前置要求

必须完成:

  • 基础入门 所有章节
  • 高级进阶 核心章节
  • ✅ 理解异步编程 (async/await)
  • ✅ 熟悉数据库操作 (SQLx 或 Diesel)
  • ✅ 了解 Web 框架基础 (Axum 或 Hyper)

建议具备:

  • 微服务架构基础概念
  • 消息队列基础概念
  • 设计模式基础(尤其是依赖注入)

📈 学习路径

高级进阶完成
    ↓
服务框架 → 依赖注入
    ↓
数据库实战 → 序列生成 → 数据处理
    ↓
消息队列 → 模板引擎
    ↓
插件系统
    ↓
毕业项目

推荐学习顺序:

  1. 架构基础 (必须先学):

    • 服务框架 → 依赖注入
  2. 数据层 (核心技能):

    • 数据库实战 → 序列生成 → 数据处理
  3. 通信层 (进阶):

    • 消息队列 → 模板引擎
  4. 扩展能力 (深入):

    • 插件系统

✅ 学习检查点

完成本部分后,你应该能够:

  • 设计服务生命周期管理
  • 实现依赖注入容器
  • 使用 SurrealDB 进行文档数据库操作
  • 集成 MQTT 消息队列
  • 生成唯一标识符 (UUID、雪花)
  • 使用 Tera/Liquid 模板引擎
  • 使用 Polars 进行数据分析
  • 设计和实现插件系统
  • 构建生产级 Rust 应用

🎓 实践项目

毕业项目建议:

  1. 微服务架构:

    • 使用服务框架构建多个微服务
    • 通过消息队列通信
    • 使用依赖注入管理依赖
  2. 数据驱动应用:

    • 使用 SurrealDB 存储数据
    • 使用 Polars 分析数据
    • 使用模板引擎生成报告
  3. 可扩展平台:

    • 实现插件系统
    • 支持动态加载插件
    • 提供服务注册和发现

📖 完整示例代码

本部分所有示例代码都来自真实项目:

样例GitHub 链接
Tera 模板tera_sample.rs
Liquid 模板liquid_sample.rs
Pest 解析器pest_sample.rs

🏆 毕业认证

完成所有精选实战章节后,你将获得:

  • ✅ Rust 高级编程能力
  • ✅ 生产级应用架构经验
  • ✅ 真实项目代码样例
  • ✅ 完整的作品集项目

➡️ 下一步

完成精选实战后,你可以:

  1. 继续深造:

  2. 实战项目:

    • 构建完整的 Web 应用
    • 创建开源 Rust 库
    • 贡献 Rust 社区
  3. 职业发展:

    • Rust 后端工程师
    • 系统程序员
    • 区块链开发

准备好了吗?让我们开始 服务框架 的学习! 🚀

数据库高级应用

开篇故事

想象你正在开发一个智能推荐系统,需要存储数百万用户的行为数据,并快速找到相似用户。传统关系型数据库擅长结构化查询,但在处理向量相似度搜索时力不从心。现代应用需要多模态数据库——既能处理传统表格数据,又能进行高效的向量检索。本章介绍两种强大的 Rust 数据库方案:SurrealDB(多模型云原生数据库)和 SQLite + sqlite-vec(轻量级向量扩展),让你的应用具备 AI 时代的核心竞争力。


本章适合谁

如果你已经掌握了 SQLx 或 Diesel 等传统 ORM,现在想探索更先进的数据库技术,本章适合你。无论你是要构建 AI 应用、处理复杂数据关系,还是需要轻量级嵌入式向量搜索,这里都有适合你的方案。


你会学到什么

完成本章后,你可以:

  1. 使用 SurrealDB 进行文档存储和关系查询
  2. 理解 SurrealDB 的 RecordID 和命名空间概念
  3. 集成 sqlite-vec 扩展进行向量相似度搜索
  4. 将向量数据库用于推荐系统和语义搜索
  5. 在多模型数据库中管理复杂数据关系
  6. 选择合适的嵌入式数据库方案

前置要求

学习本章前,你需要理解:

  • 异步编程 - async/await 语法和 Tokio 运行时
  • 结构体 - 定义和使用结构体
  • Serde 序列化 - JSON 序列化和反序列化
  • SQLx 基础 - 数据库连接和查询基础

依赖安装

运行以下命令安装所需依赖:

cargo add serde --features derive
cargo add serde_json
cargo add tokio --features full
cargo add sqlx --features runtime-tokio,postgres

第一个例子

SurrealDB 内存数据库示例

use serde::{Deserialize, Serialize};
use surrealdb::RecordId;
use surrealdb::Surreal;
use surrealdb::engine::local::Mem;

#[derive(Debug, Serialize)]
struct Person {
    title: &'static str,
    name: Name,
    marketing: bool,
}

#[derive(Debug, Serialize)]
struct Name {
    first: &'static str,
    last: &'static str,
}

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    // 创建内存数据库连接
    let db = Surreal::new::<Mem>(()).await?;
    
    // 选择命名空间和数据库
    db.use_ns("test").use_db("test").await?;
    
    // 创建记录
    let created: Option<Record> = db
        .create("person")
        .content(Person {
            title: "Founder & CEO",
            name: Name {
                first: "Tobie",
                last: "Morgan Hitchcock",
            },
            marketing: true,
        })
        .await?;
    
    println!("Created: {:?}", created);
    Ok(())
}

#[derive(Debug, Deserialize)]
struct Record {
    id: RecordId,
}

发生了什么?

  1. Surreal::new::<Mem>(()) - 创建内存数据库(无需安装服务器)
  2. use_ns("test").use_db("test") - 选择命名空间和数据库
  3. create("person") - 在 person 表中创建记录
  4. .content() - 使用 Serde 序列化 Rust 结构体
  5. RecordId - SurrealDB 自动生成的唯一标识符

原理解析

SurrealDB 架构概览

SurrealDB 是一个多模型数据库,支持文档、图、键值和关系模型:

+---------------------+     +---------------------+
|   Rust Application  |     |   SurrealDB Server  |
|                     |     |                     |
|  +---------------+  |     |  +---------------+  |
|  |  Surreal<...> |  |     |  |  Namespace    |  |
|  |  Client       |<-+---->+  |  (test)       |  |
|  +---------------+  |     |  +---------------+  |
|         |           |     |         |           |
|         | await     |     |         |           |
|         v           |     |         v           |
|  +---------------+  |     |  +---------------+  |
|  |  RecordId     |  |     |  |  Database     |  |
|  |  Typed Data   |  |     |  |  (test)       |  |
|  +---------------+  |     |  +---------------+  |
|                     |     |         |           |
+---------------------+     |         v           |
                            |  +---------------+  |
                            |  |  Tables       |  |
                            |  |  - person     |  |
                            |  |  - product    |  |
                            |  +---------------+  |
                            +---------------------+

RecordID 与记录管理

SurrealDB 使用 RecordId 唯一标识每条记录:

RecordID 格式: table_name:record_id

示例:
- person:jaime          (指定 ID)
- person:ulid()         (自动生成 ULID)
- person:uuid()         (自动生成 UUID)
- person:rand()         (随机 ID)

表 1-1: RecordID 生成方式对比

方式语法特点适用场景
指定 IDperson:jaime可预测、可读用户 ID、固定配置
ULIDperson:ulid()时间排序、唯一日志、事件
UUIDperson:uuid()全局唯一分布式系统
随机person:rand()不可预测临时数据

sqlite-vec 向量搜索原理

sqlite-vec 为 SQLite 添加向量索引能力:

+------------------+     +------------------+     +------------------+
|   查询向量       |     |   向量索引       |     |   相似度计算     |
|   [0.3, 0.3,     |     |   (vec0)         |     |   distance       |
|    0.3, 0.3]     |---->|                  |---->|                  |
+------------------+     +------------------+     +------------------+
                               |                           |
                               v                           v
                    +------------------+     +------------------+
                    |   向量表          |     |   返回 Top-K     |
                    |   vec_items       |     |   最相似记录     |
                    +------------------+     +------------------+

向量相似度算法

  • 欧几里得距离 (L2): distance = sqrt(sum((a[i] - b[i])^2))
  • 余弦相似度: 适合语义搜索
  • 内积: 适合推荐系统

常见错误

错误 1: 忘记切换命名空间

// 错误:未选择命名空间和数据库
let db = Surreal::new::<Mem>(()).await?;
// db.create("person")...  // 会失败!

// 正确:先选择 ns 和 db
db.use_ns("test").use_db("test").await?;

修复方法:创建连接后立即调用 use_ns()use_db()


错误 2: sqlite-vec 扩展未加载

// 错误:未注册扩展,vec0 表无法创建
let db = Connection::open_in_memory()?;
db.execute("CREATE VIRTUAL TABLE vec_items USING vec0(...)", []);  // 失败!

// 正确:先注册扩展
unsafe {
    sqlite3_auto_extension(Some(std::mem::transmute(sqlite3_vec_init as *const ())));
}

修复方法:使用 sqlite3_auto_extension 在连接时加载扩展


错误 3: 向量维度不匹配

// 错误:插入 3 维向量到 4 维表
let db = Connection::open_in_memory()?;
db.execute("CREATE VIRTUAL TABLE vec_items USING vec0(embedding float[4])", [])?;

let v: Vec<f32> = vec![0.1, 0.2, 0.3];  // 只有 3 维!
// 插入时会报错或产生意外结果

// 正确:确保维度一致
let v: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4];  // 4 维匹配

修复方法:创建表时指定的维度必须与插入数据一致


错误 4: 错误处理 Result 类型

// 问题:SurrealDB 和 SQLite 返回不同的 Result 类型
use surrealdb::Result;  // 这是 surrealdb::Result
use rusqlite::Result;   // 这是 rusqlite::Result

// 混淆会导致编译错误

修复方法:显式指定或使用完全限定名:

let result: surrealdb::Result<_> = db.create("person").content(data).await;

动手练习

练习 1: SurrealDB CRUD 操作

补全以下代码,实现完整的 CRUD 操作:

use surrealdb::Surreal;
use surrealdb::engine::local::Mem;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Product {
    name: String,
    price: f64,
}

async fn create_product(db: &Surreal<Mem>, name: &str, price: f64) -> surrealdb::Result<()> {
    // 补全:创建产品记录
}

async fn get_product(db: &Surreal<Mem>, id: &str) -> surrealdb::Result<Option<Product>> {
    // 补全:根据 ID 查询产品
}

async fn update_price(db: &Surreal<Mem>, id: &str, new_price: f64) -> surrealdb::Result<()> {
    // 补全:更新产品价格
}
点击查看答案
async fn create_product(db: &Surreal<Mem>, name: &str, price: f64) -> surrealdb::Result<()> {
    db.create("product")
        .content(Product {
            name: name.to_string(),
            price,
        })
        .await?;
    Ok(())
}

async fn get_product(db: &Surreal<Mem>, id: &str) -> surrealdb::Result<Option<Product>> {
    db.select(("product", id)).await
}

async fn update_price(db: &Surreal<Mem>, id: &str, new_price: f64) -> surrealdb::Result<()> {
    db.update(("product", id))
        .merge(serde_json::json!({"price": new_price}))
        .await?;
    Ok(())
}

练习 2: 向量相似度搜索

完成以下代码,实现产品推荐功能:

use rusqlite::{Connection, Result};

fn setup_vector_db(db: &Connection) -> Result<()> {
    // 创建向量表存储产品特征
    // 每个产品有 4 维特征向量
    db.execute(
        "CREATE VIRTUAL TABLE products USING vec0(features float[4])",
        [],
    )?;
    
    // 插入产品数据
    let products = vec![
        (1, vec![0.9, 0.1, 0.0, 0.0]),  // 电子产品
        (2, vec![0.1, 0.9, 0.0, 0.0]),  // 服装
        (3, vec![0.8, 0.2, 0.0, 0.0]),  // 电子产品(类似产品1)
    ];
    
    // 补全:插入产品数据
}

fn find_similar_products(db: &Connection, query_vec: Vec<f32>, limit: usize) -> Result<Vec<(i64, f64)>> {
    // 补全:查询最相似的产品
}
点击查看答案
fn setup_vector_db(db: &Connection) -> Result<()> {
    db.execute(
        "CREATE VIRTUAL TABLE products USING vec0(features float[4])",
        [],
    )?;
    
    let products = vec![
        (1, vec![0.9, 0.1, 0.0, 0.0]),
        (2, vec![0.1, 0.9, 0.0, 0.0]),
        (3, vec![0.8, 0.2, 0.0, 0.0]),
    ];
    
    let mut stmt = db.prepare("INSERT INTO products(rowid, features) VALUES (?, ?)")?;
    for (id, features) in products {
        stmt.execute(rusqlite::params![id, features.as_bytes()])?;
    }
    Ok(())
}

fn find_similar_products(db: &Connection, query_vec: Vec<f32>, limit: usize) -> Result<Vec<(i64, f64)>> {
    db.prepare(
        "SELECT rowid, distance FROM products WHERE features MATCH ?1 ORDER BY distance LIMIT ?2"
    )?
    .query_map(rusqlite::params![query_vec.as_bytes(), limit], |r| {
        Ok((r.get(0)?, r.get(1)?))
    })?
    .collect()
}

练习 3: 预测输出

以下代码的输出是什么?

use rusqlite::{Connection, Result};
use sqlite_vec::sqlite3_vec_init;
use rusqlite::ffi::sqlite3_auto_extension;

fn main() -> Result<()> {
    unsafe {
        sqlite3_auto_extension(Some(std::mem::transmute(sqlite3_vec_init as *const ())));
    }
    
    let db = Connection::open_in_memory()?;
    
    let (sqlite_ver, vec_ver): (String, String) = db.query_row(
        "SELECT sqlite_version(), vec_version()",
        [],
        |r| Ok((r.get(0)?, r.get(1)?)),
    )?;
    
    println!("SQLite: {}, vec: {}", sqlite_ver, vec_ver);
    Ok(())
}
点击查看解析

输出示例

SQLite: 3.44.0, vec: v0.1.1

解析

  1. sqlite_version() 返回 SQLite 版本号
  2. vec_version() 返回 sqlite-vec 扩展版本
  3. 两个函数都在 SQL 层可用,说明扩展正确加载

故障排查 (FAQ)

Q: SurrealDB 内存模式和持久化模式如何选择?

A: 根据使用场景选择:

// 内存模式 - 测试、临时数据
let db = Surreal::new::<Mem>(()).await?;

// RocksDB 持久化 - 生产环境
// let db = Surreal::new::<RocksDb>("/path/to/db").await?;

// 远程服务器模式
// let db = Surreal::new::<Ws>("ws://localhost:8000").await?;

选择建议

  • Mem:单元测试、CI/CD、演示
  • RocksDb:单机应用、边缘计算
  • Ws/Wss:分布式系统、多客户端

Q: sqlite-vec 适合多大的数据集?

A: sqlite-vec 适合小到中等规模:

数据规模建议方案延迟
< 10k 向量sqlite-vec< 10ms
10k - 100ksqlite-vec + 索引优化10-100ms
> 100k专用向量数据库 (pgvector, Milvus)-

Q: SurrealDB 如何与 Rust 类型系统配合?

A: 使用 Serde 实现类型安全:

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: Thing,  // SurrealDB 的 Thing 类型对应 RecordId
    name: String,
    tags: Vec<String>,  // 支持复杂类型
}

// 查询时自动反序列化
let users: Vec<User> = db.select("user").await?;

Q: 如何优化向量搜索性能?

A: 几种优化策略:

  1. 维度降维:使用 PCA 将高维向量降至 128-512 维
  2. 量化:使用 int8 替代 float32,减少 75% 存储
  3. 分区:按类别分区,缩小搜索空间
  4. 缓存:缓存热门查询结果

知识扩展 (选学)

SurrealDB 高级查询

// 使用 SurrealQL 进行复杂查询
let results = db
    .query("SELECT * FROM person WHERE marketing = true ORDER BY name.first")
    .await?;

// 带参数查询(防止注入)
let results = db
    .query("SELECT * FROM person WHERE name.first = \$name")
    .bind(("name", "Tobie"))
    .await?;

// 使用变量
db.set("min_price", 100.0).await?;
let products = db
    .query("SELECT * FROM product WHERE price > \$min_price")
    .await?;

与 SQLx 集成

SurrealDB 和 sqlite-vec 可以与 SQLx 互补使用:

// SQLx 处理关系数据
let users = sqlx::query_as::<_, User>("SELECT * FROM users")
    .fetch_all(&pool)
    .await?;

// SurrealDB 处理文档/图数据
let related = db
    .query("SELECT ->purchased->product.* FROM user:alice")
    .await?;

// sqlite-vec 处理向量搜索
let similar = find_similar_products(&sqlite_conn, query_vector, 10)?;

小结

核心要点

  1. SurrealDB 是多模型数据库,支持文档、图、关系模型
  2. RecordId 是 SurrealDB 的核心概念,支持多种 ID 生成策略
  3. sqlite-vec 为 SQLite 添加轻量级向量搜索能力
  4. 向量搜索使用距离算法(欧几里得、余弦)找到相似项
  5. 选择数据库需考虑数据规模、查询模式和部署环境

关键术语

  • RecordId: SurrealDB 记录唯一标识符
  • Namespace/Database: SurrealDB 的命名空间层级
  • 向量数据库 (Vector DB): 支持相似度搜索的数据库
  • Embedding: 文本/图像的向量表示
  • Similarity Search: 基于向量距离的内容检索

下一步


术语表

English中文
RecordId记录标识符
Namespace命名空间
Vector Search向量搜索
Embedding嵌入向量
Similarity相似度
Dimension维度
Distance Metric距离度量
Approximate Nearest Neighbor近似最近邻

完整示例:

  • SurrealDB 示例
  • SQLite 向量扩展

继续学习


💡 小知识:向量数据库的兴起

为什么需要向量数据库?

传统数据库擅长精确匹配(WHERE id = 123),但 AI 应用需要语义搜索:

用户查询: "便宜好用的耳机"

传统搜索: 匹配包含这些词的产品
向量搜索: 找到语义相似的产品
         "性价比高的蓝牙耳机"
         "平价入耳式耳机"
         "学生党耳机推荐"

应用场景

  • 语义搜索引擎
  • 商品推荐系统
  • 图像相似搜索
  • 聊天机器人检索

🌟 工业界应用:智能推荐系统

真实案例:某电商平台使用 SQLite + sqlite-vec 构建推荐系统:

// 用户行为向量
fn user_embedding(purchase_history: &[Product]) -> Vec<f32> {
    // 基于购买历史生成用户画像向量
    let mut embedding = vec![0.0; 128];
    for product in purchase_history {
        for (i, &val) in product.category_vec.iter().enumerate() {
            embedding[i] += val;
        }
    }
    normalize(&embedding)
}

// 推荐相似用户喜欢的商品
async fn recommend(
    db: &Connection,
    user_vec: Vec<f32>,
) -> Result<Vec<Product>> {
    // 找到相似用户
    let similar_users: Vec<(i64, f64)> = db
        .prepare(
            "SELECT user_id, distance FROM user_vectors WHERE embedding MATCH ? ORDER BY distance LIMIT 10"
        )?
        .query_map([user_vec.as_bytes()], |r| Ok((r.get(0)?, r.get(1)?)))?
        .collect::<Result<Vec<_>>>()?;
    
    // 返回这些用户购买的商品...
}

结果

  • 推荐准确率提升 35%
  • 系统响应时间 < 50ms
  • 单机可处理百万级用户

🧪 知识检查

问题 1 🟢 (基础概念)

SurrealDB 的 RecordId 格式是什么?

let id: RecordId = ("person", "jaime").into();

A) jaime:person B) person:jaime C) person/jaime D) jaime.person

答案与解析

答案: B) person:jaime

解析: SurrealDB 的 RecordId 格式是 table_name:record_id,用于唯一标识表中的记录。


问题 2 🟡 (向量搜索)

sqlite-vec 的 MATCH 操作返回的结果按什么排序?

SELECT rowid, distance FROM vec_items WHERE embedding MATCH ? ORDER BY distance

A) 向量 ID B) 相似度分数(越大越相似) C) 距离(越小越相似) D) 插入时间

答案与解析

答案: C) 距离(越小越相似)

解析: distance 表示查询向量与存储向量的距离(通常使用欧几里得距离),值越小表示越相似。通常需要 ORDER BY distance 获取最相似的结果。


问题 3 🔴 (多模型数据库)

以下哪个场景最适合使用 SurrealDB?

A) 只需要简单的键值存储 B) 需要文档存储 + 图关系 + 实时查询的社交应用 C) 只需要执行标准 SQL 查询的传统报表系统 D) 只需要存储二进制大对象的对象存储

答案与解析

答案: B) 需要文档存储 + 图关系 + 实时查询的社交应用

解析: SurrealDB 是多模型数据库,特别适合需要同时处理文档数据、复杂关系(图)和实时查询的应用。A 适合 Redis,C 适合传统 SQL 数据库,D 适合对象存储服务。


参考资料

  1. SurrealDB 官方文档
  2. sqlite-vec GitHub
  3. 向量数据库对比
  4. Rust Embedded Databases

记住:现代应用往往需要多种数据模型。SurrealDB 提供一站式解决方案,而 sqlite-vec 为轻量级应用带来向量能力。选择适合你场景的工具,而不是试图用一个方案解决所有问题。

服务框架与微服务架构

开篇故事

想象一下,你正在经营一家大型连锁餐厅。最初只有一家店,你自己就能管理所有的事情——采购、烹饪、服务、收银。但随着业务扩大,你开了十家店、一百家店,你不可能亲自管理每一家店的每个环节。

于是你设计了这样的架构:

  • 中央厨房(服务注册中心)统一协调所有分店的食材供应
  • 各分店(微服务)专注于自己的业务:有的专做寿司、有的专做披萨、有的专做甜品
  • 服务员(gRPC 客户端)知道如何找到对应的分店下单
  • 经理(服务框架)负责监控每个分店的运营状态,在出现问题时及时调配资源

在软件世界里,这就是微服务架构——将大型应用拆分为多个独立部署、独立扩展的小型服务。awesome crate 中的服务框架就是一套生产级的"餐厅管理系统",它包含了服务发现(Consul)、远程调用(gRPC)、依赖注入(DI)、生命周期管理等核心组件。


本章适合谁

如果你已经掌握了 Rust 基础和异步编程,现在希望构建:

  • 生产级的微服务应用
  • 支持服务注册与发现的服务集群
  • 基于 gRPC 的高性能 RPC 服务
  • 具备依赖注入和插件机制的可扩展系统

本章适合你。微服务架构是现代分布式系统的主流选择,而这些知识将帮助你构建企业级应用。


你会学到什么

完成本章后,你可以:

  1. 理解依赖注入(DI)的三种实现方式:具体类型注入、动态 trait 注入(Arc)、动态 trait 注入(Box)
  2. 使用 inventory crate 实现编译时插件注册
  3. 使用 Consul 进行服务注册、发现和配置管理
  4. 使用 Tonic 编写 gRPC 服务端和客户端
  5. 实现自定义服务生命周期框架,支持优雅启动和关闭
  6. 构建支持流式数据传输的 gRPC 服务
  7. 理解微服务架构中的健康检查和服务治理

前置要求

学习本章前,你需要理解:

  • Tokio 异步运行时 - 特别是 spawnawait、通道的使用
  • 所有权与生命周期 - 特别是 Arc、trait 对象的理解
  • 泛型与 trait - 理解泛型约束和 trait 对象
  • 线程与并发 - 了解并发基本概念

本章涉及的 crate:

  • tonic - gRPC 框架
  • consul / 自定义 HTTP 客户端 - 服务注册与发现
  • inventory - 编译时插件注册
  • async-trait - 异步 trait
  • serde - 配置序列化

依赖安装

运行以下命令安装所需依赖:

cargo add serde --features derive
cargo add serde_json
cargo add tokio --features full
cargo add axum
cargo add async-trait

第一个例子

让我们从最简单的依赖注入开始:

use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::Arc;

// 定义 Repository Trait
trait UserRepository: Any + Send + Sync {
    fn get_user(&self, id: u32) -> String;
}

// 实现具体 Repository
struct InMemoryUserRepository;
impl UserRepository for InMemoryUserRepository {
    fn get_user(&self, id: u32) -> String {
        format!("User {} from InMemoryRepo", id)
    }
}

// Service 依赖 Repository
struct UserService<R: UserRepository> {
    repo: R,
}

impl<R: UserRepository> UserService<R> {
    fn new(repo: R) -> Self {
        Self { repo }
    }
    fn greet_user(&self, id: u32) -> String {
        let user = self.repo.get_user(id);
        format!("Hello, {}!", user)
    }
}

fn main() {
    // 创建 DI 容器
    let mut services = HashMap::<TypeId, Arc<dyn Any + Send + Sync>>::new();
    
    // 注册 Repository
    services.insert(
        TypeId::of::<InMemoryUserRepository>(),
        Arc::new(InMemoryUserRepository),
    );
    
    // 注册 Service(注入依赖)
    services.insert(
        TypeId::of::<UserService<InMemoryUserRepository>>(),
        Arc::new(UserService::new(InMemoryUserRepository)),
    );
    
    // 从容器解析并调用
    let service = services
        .get(&TypeId::of::<UserService<InMemoryUserRepository>>())
        .unwrap()
        .clone()
        .downcast::<UserService<InMemoryUserRepository>>()
        .unwrap();
    
    println!("{}", service.greet_user(42));
}

完整示例:concrete_injection_sample.rs


原理解析

1. 依赖注入的三种模式

模式一:具体类型注入(Concrete Injection)

// 使用泛型参数,编译时确定类型
struct UserService<R: UserRepository> {
    repo: R,
}

优点:零运行时开销,编译时类型安全
缺点:无法运行时切换实现,容器需要为每种组合存储单独的类型

完整示例:concrete_injection_sample.rs


模式二:动态 Trait 注入(Arc 版本)

use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

// Service 使用 Arc<dyn Trait> 存储依赖
struct BusinessService {
    logger: Arc<dyn LoggerService>,
    database: Arc<dyn DatabaseService>,
}

// ServiceContainer 支持 trait 对象注册
struct ServiceContainer {
    services: Mutex<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
    factories: Mutex<HashMap<TypeId, Box<dyn Fn(&ServiceContainer) -> Arc<dyn Any + Send + Sync>>>>,
}

impl ServiceContainer {
    fn register_trait<T>(&self, service: Arc<T>)
    where
        T: ?Sized + Any + Send + Sync + 'static,
    {
        let type_id = TypeId::of::<Arc<T>>();
        let service: Arc<dyn Any + Send + Sync> = Arc::new(service);
        self.services.lock().unwrap().insert(type_id, service);
    }
    
    fn resolve_trait<T>(&self) -> Option<Arc<T>>
    where
        T: ?Sized + Send + Sync + 'static,
    {
        let type_id = TypeId::of::<Arc<T>>();
        self.services.lock().unwrap()
            .get(&type_id)
            .and_then(|s| s.clone().downcast::<Arc<T>>().ok())
            .map(|arc| (*arc).clone())
    }
}

优点:支持运行时切换实现,trait 对象共享所有权
缺点:需要处理复杂的类型擦除和 downcast

完整示例:dynmaic_injection_arc_sample.rs


模式三:动态 Trait 注入(Box 版本)

与 Arc 版本类似,但使用 Box<dyn Any> 存储:

struct ServiceContainer {
    services: Mutex<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
}

impl ServiceContainer {
    fn register<T: Sized + Any + Send + Sync + 'static>(&self, service: T) {
        let type_id = TypeId::of::<T>();
        let service: Box<dyn Any + Send + Sync> = Box::new(service);
        self.services.lock().unwrap().insert(type_id, service);
    }
}

完整示例:dynmaic_injection_box_sample.rs


2. 编译时插件注册(Inventory)

use inventory::submit;

// 定义插件 trait
trait InventoryOp: Send + Sync {
    fn name(&self) -> &'static str;
    fn execute(&self, inventory: &Mutex<HashMap<String, u32>>, item: &str, quantity: u32);
}

// 插件注册结构
#[derive(Clone, Copy)]
struct InventoryPlugin {
    name: &'static str,
    handler: &'static dyn InventoryOp,
}

// 收集所有插件
inventory::collect!(InventoryPlugin);

// 注册插件
inventory::submit! {
    InventoryPlugin {
        name: "add",
        handler: &AddItem,
    }
}

// 运行时获取所有插件
fn main() {
    for plugin in inventory::iter::<InventoryPlugin> {
        println!("Loaded plugin: {}", plugin.name);
    }
}

完整示例:inventory_sample.rs


3. Consul 服务注册与发现

服务注册(使用 rs_consul crate):

use rs_consul::{Config, Consul, RegisterEntityPayload, RegisterEntityService};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config {
        address: "http://127.0.0.1:8500".to_string(),
        ..Default::default()
    };
    let consul = Consul::new(config);
    
    // 注册服务
    let payload = RegisterEntityPayload {
        Node: "node-1".to_string(),
        Address: "192.168.1.100".to_string(),
        Service: Some(RegisterEntityService {
            Service: "my-service".to_string(),
            Port: Some(8080),
            ..Default::default()
        }),
        ..Default::default()
    };
    
    consul.register_entity(&payload).await?;
    Ok(())
}

完整示例:consul_sample.rs


自定义 Consul 客户端(生产环境):

use reqwest::Client;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct AgentServiceRegistration {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    pub name: String,
    pub address: Option<String>,
    pub port: Option<u16>,
    pub check: Option<AgentServiceCheck>,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct CatalogServiceNode {
    pub service_name: String,
    pub service_address: String,
    pub service_port: u16,
}

pub struct ConsulClient {
    http_client: Client,
    consul_api_base_url: Url,
}

impl ConsulClient {
    pub async fn register_service(&self, registration: &AgentServiceRegistration) -> Result<()> {
        let url = self.consul_api_base_url.join("agent/service/register")?;
        self.http_client
            .put(url)
            .json(registration)
            .send()
            .await?;
        Ok(())
    }
    
    pub async fn discover_service(&self, service_name: &str) -> Result<Vec<CatalogServiceNode>> {
        let url = self.consul_api_base_url
            .join(&format!("catalog/service/{}", service_name))?;
        let nodes: Vec<CatalogServiceNode> = self.http_client
            .get(url)
            .send()
            .await?
            .json()
            .await?;
        Ok(nodes)
    }
}

完整代码:framework/registry.rs


4. gRPC 服务(Tonic)

基础服务端:

mod helloworld {
    tonic::include_proto!("helloworld");
}

use helloworld::greeter_server::{Greeter, GreeterServer};
use helloworld::{HelloReply, HelloRequest};

#[derive(Default)]
pub struct MyGreeter;

#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloReply>, Status> {
        let reply = HelloReply {
            message: format!("Hello {}!", request.into_inner().name),
        };
        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;
    let greeter = MyGreeter::default();
    
    tonic::transport::Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;
    Ok(())
}

完整示例:tonic_hello_server.rs


基础客户端:

use helloworld::greeter_client::GreeterClient;
use helloworld::HelloRequest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = GreeterClient::connect("http://[::1]:50051").await?;
    
    let request = tonic::Request::new(HelloRequest {
        name: "Tonic".into(),
    });
    
    let response = client.say_hello(request).await?;
    println!("RESPONSE={:?}", response.into_inner().message);
    Ok(())
}

完整示例:tonic_hello_client.rs


流式 gRPC 服务:

type WatchStream = Pin<Box<dyn Stream<Item = Result<Item, Status>> + Send>>;

#[tonic::async_trait]
impl Inventory for StoreInventory {
    async fn watch(
        &self,
        request: Request<ItemIdentifier>,
    ) -> Result<Response<Self::WatchStream>, Status> {
        let (tx, rx) = mpsc::unbounded_channel();
        let inventory = self.inventory.clone();
        let id = request.into_inner();
        
        // 后台任务监控库存变化
        tokio::spawn(async move {
            loop {
                tokio::time::sleep(Duration::from_secs(1)).await;
                
                let map = inventory.lock().await;
                if let Some(item) = map.get(&id.sku) {
                    if tx.send(Ok(item.clone())).is_err() {
                        break;
                    }
                }
            }
        });
        
        let stream = UnboundedReceiverStream::new(rx);
        Ok(Response::new(Box::pin(stream)))
    }
}

完整示例:tonic_store_server.rs
客户端:tonic_store_client.rs


5. 服务生命周期框架

架构概览:

┌─────────────────────────────────────────────────────────────┐
│                 ApplicationFramework<S>                      │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                  RunnableService                      │  │
│  │   ┌─────────┐   ┌─────────┐   ┌─────────────────┐    │  │
│  │   │ gRPC    │   │ HTTP    │   │ Health Check    │    │  │
│  │   │ Server  │   │ Server  │   │ Endpoint        │    │  │
│  │   └────┬────┘   └────┬────┘   └─────────────────┘    │  │
│  │        └─────────────┴─────────────┘                  │  │
│  │                      │                                │  │
│  │              Consul Registration                      │  │
│  └───────────────────────────────────────────────────────┘  │
│                           │                                  │
│                    ┌──────┴──────┐                          │
│                    │   oneshot   │ ← 优雅关闭信号            │
│                    │   channel   │                          │
│                    └─────────────┘                          │
└─────────────────────────────────────────────────────────────┘

RunnableService trait:

use async_trait::async_trait;
use tokio::sync::{oneshot, RwLock};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceStatus {
    Initializing,
    Starting,
    Running,
    Stopping,
    Stopped,
    Failed(String),
}

#[async_trait]
pub trait RunnableService: Send + Sync + 'static {
    type Config: ServiceConfig;
    
    fn new(config: Self::Config, instance_id: String, status_arc: Arc<RwLock<ServiceStatus>>) -> Self;
    fn instance_id(&self) -> &str;
    fn get_status(&self) -> Arc<RwLock<ServiceStatus>>;
    
    // 核心逻辑:接收关闭信号,运行到结束
    async fn start_service_logic(
        &self,
        shutdown_rx: oneshot::Receiver<()>,
    ) -> Result<(), FrameworkError>;
}

应用框架实现:

pub struct ApplicationFramework<S: RunnableService> {
    service_instance: Arc<S>,
    status_receiver: Arc<RwLock<ServiceStatus>>,
    shutdown_tx: Option<oneshot::Sender<()>>,
    service_handle: Option<JoinHandle<Result<(), FrameworkError>>>,
}

impl<S: RunnableService> ApplicationFramework<S> {
    pub fn new(config: S::Config) -> Result<Self, FrameworkError> {
        let instance_id = format!("{}-{}", config.base_config().service_id_prefix, Uuid::new_v4());
        let status_arc = Arc::new(RwLock::new(ServiceStatus::Initializing));
        let (tx_shutdown, rx_shutdown) = oneshot::channel();
        
        let service_instance = Arc::new(S::new(config, instance_id, Arc::clone(&status_arc)));
        let service_for_task = Arc::clone(&service_instance);
        
        // Spawn 服务主逻辑
        let service_handle = tokio::spawn(async move {
            // 启动 Consul 注册
            // 启动 gRPC 和 HTTP 服务器
            // 等待关闭信号
            service_for_task.start_service_logic(rx_shutdown).await
        });
        
        Ok(Self { /* ... */ })
    }
    
    pub async fn stop(&mut self) -> Result<(), FrameworkError> {
        if let Some(tx) = self.shutdown_tx.take() {
            let _ = tx.send(()); // 发送关闭信号
        }
        // 等待服务完全停止
        if let Some(handle) = self.service_handle.take() {
            handle.await?;
        }
        Ok(())
    }
}

完整代码:

  • framework/lifecycle.rs
  • framework/config.rs
  • framework/error.rs

生产级服务实现(GreeterService):

#[async_trait]
impl RunnableService for GreeterApplicationService {
    type Config = GreeterServiceConfig;
    
    async fn start_service_logic(
        &self,
        shutdown_rx: oneshot::Receiver<()>,
    ) -> Result<(), FrameworkError> {
        // 1. 构建 gRPC 服务
        let greeter_service = GreeterServer::new(MyGreeter::default());
        let grpc_server = tonic::transport::Server::builder()
            .layer(TraceLayer::new_for_grpc())
            .add_service(greeter_service)
            .serve(grpc_addr);
        
        // 2. 启动 HTTP 健康检查
        let health_app = axum::Router::new()
            .route("/health", axum::routing::get(|| async { StatusCode::OK }));
        let health_server = axum::serve(listener, health_app);
        
        // 3. 注册到 Consul
        self.consul_client.register_service(&registration_payload).await?;
        
        // 4. 并发运行,等待关闭信号
        tokio::select! {
            _ = grpc_server => {},
            _ = health_server => {},
            _ = shutdown_rx => {},
        }
        
        // 5. 从 Consul 注销
        self.consul_client.deregister_service(&self.instance_id).await?;
        Ok(())
    }
}

完整示例:greeter_service.rs
服务消费者:greeter_consume.rs


常见错误

错误 1:在 async 上下文中使用 std::sync::Mutex

// ❌ 错误:在 async 函数中使用阻塞锁
async fn bad_example(data: Arc<std::sync::Mutex<i32>>) {
    let mut guard = data.lock().unwrap(); // 阻塞整个线程!
    some_async_op().await; // 锁被持有跨越 await 点
    *guard += 1;
}

// ✅ 正确:使用 tokio::sync::Mutex
async fn good_example(data: Arc<tokio::sync::Mutex<i32>>) {
    let mut guard = data.lock().await; // 异步锁
    some_async_op().await; // 锁自动释放和重新获取
    *guard += 1;
}

错误 2:服务发现时没有处理空列表

// ❌ 危险:没有检查服务列表为空
let nodes = consul_client.discover_service("my-service").await?;
let node = &nodes[0]; // panic if empty!

// ✅ 安全:检查空列表
let nodes = consul_client.discover_service("my-service").await?;
if nodes.is_empty() {
    tokio::time::sleep(Duration::from_secs(5)).await;
    return Ok(()); // 或者重试
}
let node = &nodes[index % nodes.len()]; // 轮询

错误 3:忘记在关闭时从 Consul 注销服务

// ❌ 错误:服务关闭后仍残留在 Consul
async fn run_server() {
    consul.register(&service).await.unwrap();
    server.await; // 服务停止后没有注销
}

// ✅ 正确:使用 drop guard 或 select
async fn run_server() {
    consul.register(&service).await.unwrap();
    tokio::select! {
        _ = server => {},
        _ = shutdown_rx => {},
    }
    consul.deregister(&service.id).await.unwrap(); // 确保注销
}

错误 4:gRPC 流没有正确处理客户端断开

// ❌ 问题:客户端断开后继续发送
while let Some(item) = rx.recv().await {
    tx.send(Ok(item)).await?; // 可能无限阻塞
}

// ✅ 正确:使用 try_send 或检查发送结果
tokio::spawn(async move {
    loop {
        if let Some(item) = map.get(&id.sku) {
            if tx.send(Ok(item.clone())).is_err() {
                return; // 客户端断开,停止任务
            }
        }
        tokio::time::sleep(Duration::from_secs(1)).await;
    }
});

动手练习

练习 1:实现服务注册

补全以下代码,实现服务注册到 Consul:

async fn register_service(
    consul: &ConsulClient,
    name: &str,
    address: &str,
    port: u16,
) -> Result<()> {
    let registration = AgentServiceRegistration {
        id: Some(format!("{}-{}", name, Uuid::new_v4())),
        name: name.to_string(),
        address: Some(address.to_string()),
        port: Some(port),
        // TODO: 添加健康检查
        check: Some(AgentServiceCheck {
            http: Some(format!("http://{}:{}/health", address, port + 1)),
            interval: Some("10s".to_string()),
            timeout: Some("1s".to_string()),
            ..Default::default()
        }),
    };
    
    // TODO: 调用 consul.register_service
}
点击查看答案
async fn register_service(
    consul: &ConsulClient,
    name: &str,
    address: &str,
    port: u16,
) -> Result<()> {
    let registration = AgentServiceRegistration {
        id: Some(format!("{}-{}", name, Uuid::new_v4())),
        name: name.to_string(),
        address: Some(address.to_string()),
        port: Some(port),
        check: Some(AgentServiceCheck {
            http: Some(format!("http://{}:{}/health", address, port + 1)),
            interval: Some("10s".to_string()),
            timeout: Some("1s".to_string()),
            deregister_critical_service_after: Some("1m".to_string()),
            ..Default::default()
        }),
    };
    
    consul.register_service(&registration).await?;
    Ok(())
}

练习 2:实现轮询负载均衡

补全以下代码,实现基于 Consul 的轮询服务发现:

pub struct LoadBalancer {
    consul: ConsulClient,
    service_name: String,
    index: AtomicUsize,
}

impl LoadBalancer {
    pub async fn next(&self) -> Result<CatalogServiceNode> {
        // TODO: 获取服务列表并返回下一个节点
    }
}
点击查看答案
use std::sync::atomic::{AtomicUsize, Ordering};

impl LoadBalancer {
    pub async fn next(&self) -> Result<CatalogServiceNode> {
        let nodes = self.consul.discover_service(&self.service_name).await?;
        if nodes.is_empty() {
            return Err(anyhow!("No available service instances"));
        }
        
        let index = self.index.fetch_add(1, Ordering::Relaxed);
        let node = &nodes[index % nodes.len()];
        Ok(node.clone())
    }
}

练习 3:实现优雅关闭

补全以下代码,在收到 Ctrl+C 信号时优雅关闭服务:

#[tokio::main]
async fn main() -> Result<()> {
    let framework = ApplicationFramework::<MyService>::new(config)?;
    
    // TODO: 监听 Ctrl+C 信号并调用 framework.stop()
}
点击查看答案
use tokio::signal;

#[tokio::main]
async fn main() -> Result<()> {
    let mut framework = ApplicationFramework::<MyService>::new(config)?;
    
    tokio::select! {
        _ = signal::ctrl_c() => {
            println!("Received shutdown signal...");
            framework.stop().await?;
        }
        status = watch_service_status(&framework) => {
            println!("Service exited with status: {:?}", status);
        }
    }
    
    Ok(())
}

实际应用

应用场景 1:微服务架构的完整链路

// 1. 服务提供者:GreeterService
pub struct GreeterApplicationService {
    config: GreeterServiceConfig,
    consul_client: ConsulClient,
    // ...
}

// 2. 服务注册(启动时)
async fn start(&self) -> Result<()> {
    // 注册到 Consul
    self.consul_client.register_service(&AgentServiceRegistration {
        name: "greeter-service".to_string(),
        address: Some(self.config.service_ip.clone()),
        port: Some(self.config.grpc_port),
        check: Some(health_check),
        ..Default::default()
    }).await?;
    
    // 启动 gRPC 服务
    tonic::transport::Server::builder()
        .add_service(GreeterServer::new(MyGreeter::default()))
        .serve(addr)
        .await?;
}

// 3. 服务消费者:GreeterClient
async fn call_greeter(consul: &ConsulClient) -> Result<String> {
    // 服务发现
    let nodes = consul.discover_service("greeter-service").await?;
    let node = &nodes[0];
    
    // 创建连接
    let addr = format!("http://{}:{}", node.service_address, node.service_port);
    let mut client = GreeterClient::connect(addr).await?;
    
    // 调用 RPC
    let response = client.say_hello(HelloRequest { name: "World".into() }).await?;
    Ok(response.into_inner().message)
}

完整示例:greeter_service.rs / greeter_consume.rs


应用场景 2:带缓存的服务发现

pub struct CachedServiceDiscovery {
    consul: ConsulClient,
    cache: Arc<RwLock<HashMap<String, (Vec<CatalogServiceNode>, Instant)>>>,
    ttl: Duration,
}

impl CachedServiceDiscovery {
    pub async fn discover(&self, service_name: &str) -> Result<Vec<CatalogServiceNode>> {
        // 检查缓存
        if let Some((nodes, timestamp)) = self.cache.read().await.get(service_name) {
            if timestamp.elapsed() < self.ttl {
                return Ok(nodes.clone());
            }
        }
        
        // 从 Consul 获取
        let nodes = self.consul.discover_service(service_name).await?;
        
        // 更新缓存
        self.cache.write().await.insert(
            service_name.to_string(),
            (nodes.clone(), Instant::now()),
        );
        
        Ok(nodes)
    }
}

应用场景 3:插件化架构

// 使用 inventory 实现编译时插件注册
inventory::collect!(Plugin);

pub struct Plugin {
    name: &'static str,
    handler: fn(&Context) -> Result<()>,
}

// 各插件自行注册
inventory::submit! {
    Plugin { name: "logger", handler: logger_plugin }
}

inventory::submit! {
    Plugin { name: "metrics", handler: metrics_plugin }
}

// 主程序加载所有插件
fn main() {
    let ctx = Context::new();
    for plugin in inventory::iter::<Plugin> {
        println!("Loading plugin: {}", plugin.name);
        (plugin.handler)(&ctx).unwrap();
    }
}

完整示例:inventory_sample.rs


故障排查 (FAQ)

Q: Consul 服务注册成功但发现为空?

A: 检查以下几点:

  1. 服务名称是否完全匹配(大小写敏感)
  2. Consul 的健康检查是否通过(失败的服务会被过滤)
  3. 是否等待了足够的时间(服务注册有延迟)
// 调试技巧:打印所有已注册服务
let services = consul.get_all_registered_service_names(None).await?;
println!("Registered services: {:?}", services);

Q: gRPC 客户端连接失败?

A: 常见原因:

  1. 地址格式:必须使用 http:// 前缀
  2. 端口错误:检查服务端的监听端口
  3. TLS 配置:如果使用 TLS,需要配置证书
// ✅ 正确
GreeterClient::connect("http://127.0.0.1:50051").await?;

// ❌ 错误:缺少协议前缀
GreeterClient::connect("127.0.0.1:50051").await?;

Q: 依赖注入时 downcast 失败?

A: TypeId 是编译期确定的,确保注册和解析时使用完全相同的类型:

// ✅ 一致:注册和解析使用相同类型
container.register_trait::<dyn LoggerService>(Arc::new(ConsoleLogger));
let logger = container.resolve_trait::<dyn LoggerService>();

// ❌ 不一致:类型不同
container.register::<ConsoleLogger>(ConsoleLogger); // TypeId::of::<ConsoleLogger>()
let service = container.resolve::<dyn LoggerService>(); // TypeId::of::<dyn LoggerService>()

Q: 服务框架启动后立即退出?

A: 确保你的 start_service_logic 是"长期运行"的:

async fn start_service_logic(&self, shutdown_rx: oneshot::Receiver<()>) -> Result<()> {
    // ❌ 错误:立即返回
    tokio::spawn(async { /* ... */ });
    Ok(()) // 任务还在运行,但函数已返回
    
    // ✅ 正确:等待信号
    let server_future = tokio::spawn(server);
    tokio::select! {
        _ = server_future => {},
        _ = shutdown_rx => {},
    }
    Ok(())
}

知识扩展 (选学)

Tonic 中间件

使用 Tower 中间件添加通用功能:

use tower::{ServiceBuilder, timeout::TimeoutLayer};
use tower_http::trace::TraceLayer;

let layer = ServiceBuilder::new()
    .layer(TraceLayer::new_for_grpc())
    .layer(TimeoutLayer::new(Duration::from_secs(30)))
    .into_inner();

Server::builder()
    .layer(layer)
    .add_service(GreeterServer::new(MyGreeter::default()))
    .serve(addr)
    .await?;

gRPC 反射服务

添加反射支持,方便调试:

let reflection_service = tonic_reflection::server::Builder::configure()
    .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
    .build_v1()?;

Server::builder()
    .add_service(InventoryServer::new(inventory))
    .add_service(reflection_service)
    .serve(addr)
    .await?;

完整示例:tonic_store_server.rs


配置热更新

pub struct ConfigWatcher {
    path: PathBuf,
    current: Arc<RwLock<BaseServiceConfig>>,
}

impl ConfigWatcher {
    pub async fn watch(&self) -> Result<()> {
        let mut interval = tokio::time::interval(Duration::from_secs(10));
        loop {
            interval.tick().await;
            
            let content = tokio::fs::read_to_string(&self.path).await?;
            let new_config: BaseServiceConfig = serde_json::from_str(&content)?;
            
            *self.current.write().await = new_config;
            println!("Config updated");
        }
    }
}

小结

核心要点:

  1. 依赖注入有三种模式:具体类型注入(零开销)、动态 Arc 注入(灵活共享)、动态 Box 注入(独占所有权)
  2. Service Container 使用 TypeId 进行类型安全的运行时服务查找
  3. Consul 提供服务注册、发现、健康检查和配置管理
  4. Tonic 是 Rust 生态中功能完整的 gRPC 框架,支持 unary、streaming、拦截器等
  5. ApplicationFramework 通过 RunnableService trait 统一管理服务生命周期
  6. 优雅关闭使用 oneshot::channel 传递信号,确保资源正确释放

关键术语:

English中文
Dependency Injection (DI)依赖注入
Service Registry服务注册中心
Service Discovery服务发现
Health Check健康检查
gRPCgRPC 远程过程调用
Unary RPC一元 RPC
Streaming RPC流式 RPC
Graceful Shutdown优雅关闭
Circuit Breaker熔断器
Load Balancing负载均衡

下一步:

  • 探索 数据库访问 - 与 SQLx 和 Diesel 集成
  • 学习 Tokio - 深入理解异步运行时
  • 查看 Axum - 构建 HTTP REST API

术语表

English中文
Dependency Injection依赖注入
Service Container服务容器
TypeId类型标识
Trait ObjectTrait 对象
Service Registry服务注册表
Service Discovery服务发现
ConsulConsul 服务治理工具
gRPCGoogle RPC 框架
TonicRust gRPC 实现
ProtobufProtocol Buffers 序列化
Unary一元调用
Streaming流式传输
RunnableService可运行服务 trait
ApplicationFramework应用框架
Graceful Shutdown优雅关闭
Health Check健康检查
Round-robin轮询算法
Inventory编译时插件注册

知识检查点

检查点 1 🟢 (DI 基础)

以下代码的输出是什么?

use std::any::TypeId;

trait Service: Any + Send + Sync {}
struct Logger;
impl Service for Logger {}

fn main() {
    let id1 = TypeId::of::<Logger>();
    let id2 = TypeId::of::<dyn Service>();
    println!("{}", id1 == id2);
}

A) true
B) false
C) 编译错误
D) 运行时 panic

答案与解析

答案: B) false

解析:

  • TypeId::of::<Logger>() 是具体类型的 TypeId
  • TypeId::of::<dyn Service>() 是 trait 对象的 TypeId
  • 两者在编译期生成不同的标识符
  • 这是依赖注入容器必须使用一致类型的原因

检查点 2 🟡 (Consul 服务发现)

以下代码有什么问题?

async fn call_service(consul: &ConsulClient) -> Result<String> {
    let nodes = consul.discover_service("my-service").await?;
    let node = &nodes[0]; // 第 0 行
    let addr = format!("http://{}:{}", node.service_address, node.service_port);
    // ...
}

A) 没有问题
B) 如果服务列表为空会 panic
C) 应该使用轮询而不是固定取第一个
D) B 和 C 都正确

答案与解析

答案: D) B 和 C 都正确

解析:

  1. 空列表问题:如果 Consul 返回空列表,nodes[0] 会导致 panic,应该使用 nodes.get(0) 或检查 is_empty()
  2. 负载均衡:生产环境应该使用轮询(round-robin)或随机选择,而不是固定取第一个,以实现负载分散

修复方案:

if nodes.is_empty() {
    return Err(anyhow!("No service available"));
}
let idx = rand::random::<usize>() % nodes.len();
let node = &nodes[idx];

检查点 3 🔴 (生命周期与优雅关闭)

以下代码为什么可能导致资源泄漏?

#[async_trait]
impl RunnableService for MyService {
    async fn start_service_logic(
        &self,
        shutdown_rx: oneshot::Receiver<()>,
    ) -> Result<(), FrameworkError> {
        // 注册到 Consul
        self.consul.register_service(&self.registration).await?;
        
        // 启动服务器
        self.server.serve(addr).await?;
        
        // 从 Consul 注销
        self.consul.deregister_service(&self.id).await?;
        Ok(())
    }
}

A) Consul 注册应该在构造函数中完成
B) 如果服务器 panic,不会执行注销代码
C) 缺少对 shutdown_rx 的处理
D) B 和 C 都正确

答案与解析

答案: D) B 和 C 都正确

解析:

  1. panic 处理:如果 self.server.serve() panic,注销代码不会执行,导致"僵尸服务"残留在 Consul
  2. 缺少 shutdown 处理:没有响应 shutdown_rx 信号,无法接受优雅关闭指令

修复方案:

async fn start_service_logic(&self, shutdown_rx: oneshot::Receiver<()>) -> Result<(), FrameworkError> {
    self.consul.register_service(&self.registration).await?;
    
    tokio::select! {
        result = self.server.serve(addr) => {
            result?;
        }
        _ = shutdown_rx => {
            info!("Shutdown signal received");
        }
    }
    
    // 确保注销被执行(即使前面的代码 panic,这里不会执行,需要额外处理)
    self.consul.deregister_service(&self.id).await?;
    Ok(())
}

扩展阅读

官方资源

相关项目

  • Tower - 服务中间件抽象
  • Hyper - HTTP 实现,Tonic 底层使用
  • Prost - Protocol Buffers 实现
  • Arc-swap - 零拷贝配置热更新

进阶主题

  • Service Mesh: Istio、Linkerd 等服务网格技术
  • gRPC 流控: 背压(backpressure)和流控策略
  • 分布式追踪: OpenTelemetry、Jaeger 集成
  • 服务熔断: 使用 tower::limit 实现熔断和限流

继续学习

  • 下一步:Tokio 异步运行时
  • 进阶:Axum Web 框架
  • 相关:数据库访问

💡 记住:微服务架构的核心是解耦和自治——每个服务独立部署、独立扩展,通过标准协议(gRPC/HTTP)通信。服务框架让这些理念在 Rust 中变得可实现!

依赖注入 (Dependency Injection)

开篇故事

想象你在组装一台电脑。如果你把 CPU、内存、硬盘全部焊死在主板上(硬编码依赖),那么升级任何一个部件都需要更换整块主板。但如果你使用插槽和接口(依赖注入),你可以随时更换任何部件,而无需改动主板。

在软件中,依赖注入就是这种"插槽"机制——服务不自己创建依赖,而是通过外部"注入"依赖。这让代码更易测试、更易维护、更易扩展。


本章适合谁

如果你在构建中大型 Rust 应用,需要管理服务之间的依赖关系、实现可测试的代码架构、或者理解 Rust 中的 DI 模式,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 理解 Rust 中依赖注入的三种主要模式
  2. 使用具体类型注入(Concrete Injection)
  3. 使用 Arc 和 Box 实现动态依赖注入
  4. 构建 Service Container(服务容器)
  5. 使用工厂模式实现延迟初始化
  6. 解析具体服务和 Trait 对象

前置要求


依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add anyhow

第一个例子

最简单的具体类型注入:

// 定义 Repository Trait
trait UserRepository {
    fn get_user(&self, id: u32) -> String;
}

// 具体实现
struct InMemoryUserRepository;

impl UserRepository for InMemoryUserRepository {
    fn get_user(&self, id: u32) -> String {
        format!("User {}", id)
    }
}

// Service 依赖注入
struct UserService<R: UserRepository> {
    repo: R,
}

impl<R: UserRepository> UserService<R> {
    fn new(repo: R) -> Self {
        Self { repo }
    }

    fn greet_user(&self, id: u32) -> String {
        let user = self.repo.get_user(id);
        format!("Hello, {}!", user)
    }
}

// 使用
let repo = InMemoryUserRepository;
let service = UserService::new(repo);
println!("{}", service.greet_user(42));

完整示例:crates/awesome/src/services/concrete_injection_sample.rs


原理解析

Rust 依赖注入的三种模式

Rust 依赖注入
├── 模式 1: 具体类型注入 (Concrete Injection)
│   ├── 使用泛型参数
│   ├── 编译时确定类型
│   └── 零运行时开销
├── 模式 2: Trait 对象注入 (Dynamic Injection)
│   ├── 使用 Arc<dyn Trait> 或 Box<dyn Trait>
│   ├── 运行时确定类型
│   └── 轻微动态分发开销
└── 模式 3: 服务容器 (Service Container)
    ├── 基于 TypeId 的注册/解析
    ├── 支持工厂模式
    └── 类似 Spring/IoC 容器

模式 1: 具体类型注入

原理:使用泛型参数,在编译时确定具体类型。

// 编译时确定类型,零运行时开销
struct UserService<R: UserRepository> {
    repo: R,  // 具体类型,非 trait 对象
}

优点

  • 零运行时开销(单态化)
  • 编译时类型安全
  • 内联优化

缺点

  • 每种类型组合生成独立代码
  • 无法在运行时切换实现

适用场景:性能关键路径、简单依赖关系

模式 2: Arc 动态注入

原理:使用 Arc<dyn Trait> 包装 trait 对象,支持运行时切换。

struct BusinessService {
    logger: Arc<dyn LoggerService>,      // 动态分发
    database: Arc<dyn DatabaseService>,  // 动态分发
}

优点

  • 运行时可切换实现
  • 共享所有权(Arc)
  • 线程安全(Send + Sync)

缺点

  • 虚表查找开销
  • 引用计数开销

适用场景:需要运行时灵活性、多实现切换

模式 3: Box 动态注入

原理:与 Arc 类似,但使用 Box 独占所有权。

struct ServiceContainer {
    services: Mutex<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
}

Arc vs Box 选择

  • Arc: 多服务共享同一实例
  • Box: 每个服务独占实例

模式 4: 服务容器 (Service Container)

原理:基于 TypeId 的注册/解析系统,类似 Spring/IoC 容器。

struct ServiceContainer {
    services: Mutex<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
    factories: Mutex<HashMap<TypeId, Box<dyn Fn(&ServiceContainer) -> Arc<dyn Any + Send + Sync>>>>,
}

impl ServiceContainer {
    // 注册具体服务
    fn register<T: Any + Send + Sync + 'static>(&self, service: T) {
        let type_id = TypeId::of::<T>();
        self.services.lock().unwrap()
            .insert(type_id, Arc::new(service));
    }

    // 注册工厂(延迟初始化)
    fn register_factory<T, F>(&self, factory: F)
    where
        T: Service + 'static,
        F: Fn(&ServiceContainer) -> Arc<T> + Send + Sync + 'static,
    {
        let type_id = TypeId::of::<T>();
        let wrapped_factory = Box::new(move |container| factory(container));
        self.factories.lock().unwrap().insert(type_id, wrapped_factory);
    }

    // 解析服务
    fn resolve<T: Service + 'static>(&self) -> Option<Arc<T>> {
        let type_id = TypeId::of::<T>();
        // 先查缓存,再查工厂
        ...
    }
}

完整使用示例

let container = Arc::new(ServiceContainer::new());

// 1. 注册具体服务
container.register(ConsoleLogger);
container.register(InMemoryDatabase);

// 2. 注册 trait 对象
container.register::<Arc<dyn LoggerService>>(
    Arc::new(ConsoleLogger) as Arc<dyn LoggerService>
);

// 3. 注册工厂(自动解析依赖)
container.register_factory::<BusinessService, _>(|container| {
    let logger = container.resolve::<ConsoleLogger>()
        .expect("Logger not found");
    let database = container.resolve::<InMemoryDatabase>()
        .expect("Database not found");
    Arc::new(BusinessService::new(logger, database))
});

// 4. 解析并使用
let business = container.resolve::<BusinessService>()
    .expect("BusinessService not found");
business.perform_task("Process data");

完整示例:

工厂模式与延迟初始化

// 工厂在首次 resolve 时执行,之后缓存结果
container.register_factory::<BusinessService, _>(|container| {
    // 自动解析依赖
    let logger = container.resolve::<ConsoleLogger>().unwrap();
    let database = container.resolve::<InMemoryDatabase>().unwrap();
    Arc::new(BusinessService::new(logger, database))
});

优势

  • 按需创建(非启动时全部创建)
  • 自动解析依赖链
  • 缓存结果(单次初始化)

Rust DI 生态对比

方案类型特点适用场景
手写容器运行时灵活,无额外依赖中大型项目
[shaku]运行时类型安全,编译时检查需要严格类型
[inject]编译时宏驱动,零运行时开销追求性能
[poem/inject]运行时Web 框架集成Web 应用

推荐:对于大多数项目,手写 Service Container 已足够。Rust 社区更倾向于显式依赖传递而非重型 DI 框架。


常见错误

错误 1: 在异步上下文中使用 std::sync::Mutex

// ❌ 错误:阻塞异步运行时
struct ServiceContainer {
    services: std::sync::Mutex<HashMap<TypeId, Arc<dyn Any>>>,
}

// ✅ 正确:使用 tokio::sync::Mutex
struct ServiceContainer {
    services: tokio::sync::Mutex<HashMap<TypeId, Arc<dyn Any>>>,
}

错误 2: 循环依赖

// A 依赖 B,B 依赖 A → 死锁
// 解决:引入事件总线或消息队列解耦

错误 3: Trait 对象类型不匹配

// ❌ 错误:TypeId 不匹配
container.register::<Arc<dyn LoggerService>>(Arc::new(ConsoleLogger));
// 注册和解析必须使用相同的 TypeId

// ✅ 正确:确保注册和解析类型一致
container.register_trait::<dyn LoggerService>(Arc::new(ConsoleLogger) as Arc<dyn LoggerService>);
let logger = container.resolve_trait::<dyn LoggerService>();

知识检查

问题 1: Rust 中依赖注入的三种主要模式是什么?

问题 2: Arc 和 Box 在 DI 中有什么区别?

问题 3: 工厂模式的优势是什么?

点击查看答案与解析
  1. 具体类型注入(泛型)、Trait 对象注入(Arc/Box)、服务容器(TypeId)
  2. Arc 支持共享所有权(多服务共享),Box 独占所有权(单服务使用)
  3. 延迟初始化、自动解析依赖链、缓存结果

关键理解: Rust 的 DI 更注重显式和类型安全,而非魔法。


延伸阅读

学习完依赖注入后,你可能还想了解:

选择建议:

  • 简单项目 → 具体类型注入(泛型)
  • 中型项目 → Arc 动态注入
  • 大型项目 → 服务容器 + 工厂模式

小结

核心要点:具体注入零开销、Arc 支持共享、Service Container 最灵活、工厂模式延迟初始化

完整示例:crates/awesome/src/services/

插件系统 (Plugin System)

开篇故事

想象你在开发一个图像编辑器。最初只支持 JPEG 和 PNG 格式。但随着用户需求增长,你需要支持 WebP、AVIF、HEIC 等新格式。如果每次都要修改核心代码、重新编译整个应用,开发效率会非常低。

插件系统就像给应用预留了"扩展插槽"——新功能可以作为独立模块插入,无需修改核心代码。在 Rust 中,由于编译时类型安全和无运行时反射的特性,实现插件系统需要特殊的设计模式。本章将介绍 Rust 中实现插件系统的多种方案。


本章适合谁

如果你想学习:

  • Rust 中如何实现可扩展的插件架构
  • 编译时插件注册 vs 运行时动态加载的区别
  • 如何设计可插拔的服务架构

本章适合你。插件系统是构建可扩展应用的核心技术。


你会学到什么

完成本章后,你可以:

  1. 理解 Rust 插件系统的三种主要实现方式
  2. 使用 inventory crate 实现编译时插件注册
  3. 使用 libloading crate 实现运行时动态加载
  4. 使用 dlopen crate 实现 C 兼容的动态库
  5. 根据场景选择合适的插件方案
  6. 设计可插拔的服务架构

前置要求

  • 特征 - trait 定义和实现
  • 依赖注入 - 服务容器模式
  • 理解动态库编译(cdylib

依赖安装

不同插件方案需要不同的依赖:

# 方案 1: 编译时插件注册
cargo add inventory

# 方案 2: 运行时动态加载
cargo add libloading

# 方案 3: C 兼容动态库
cargo add dlopen

第一个例子

使用 inventory crate 实现编译时插件注册:

use inventory::submit;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

// 1. 定义插件 trait
trait InventoryOp: Send + Sync {
    fn name(&self) -> &'static str;
    fn execute(&self, inventory: &Mutex<HashMap<String, u32>>, item: &str, quantity: u32);
}

// 2. 定义插件注册结构体
#[derive(Clone, Copy)]
struct InventoryPlugin {
    name: &'static str,
    handler: &'static dyn InventoryOp,
}

// 3. 收集所有提交的插件
inventory::collect!(InventoryPlugin);

// 4. 提交插件(可以在任何文件中)
struct AddItem;
impl InventoryOp for AddItem {
    fn name(&self) -> &'static str { "add" }
    fn execute(&self, inv: &Mutex<HashMap<String, u32>>, item: &str, qty: u32) {
        let mut inv = inv.lock().unwrap();
        let current = inv.get(item).copied().unwrap_or(0);
        inv.insert(item.to_string(), current + qty);
        println!("Added {} {}", qty, item);
    }
}

inventory::submit! {
    InventoryPlugin { name: "add", handler: &AddItem }
}

// 5. 主程序:遍历并执行插件
fn main() {
    let inventory = Arc::new(Mutex::new(HashMap::new()));
    
    // 收集所有注册的插件
    let mut plugins: HashMap<&str, &dyn InventoryOp> = HashMap::new();
    for plugin in inventory::iter::<InventoryPlugin> {
        if !plugin.name.is_empty() {
            plugins.insert(plugin.name, plugin.handler);
        }
    }
    
    // 执行插件
    if let Some(op) = plugins.get("add") {
        op.execute(&inventory, "apple", 5);
    }
}

完整示例:crates/awesome/src/services/inventory_sample.rs


原理解析

Rust 插件系统的三种方案

Rust 插件系统
├── 方案 1: 编译时插件注册 (inventory)
│   ├── 原理: 链接时收集所有 submit! 宏提交的插件
│   ├── 优点: 类型安全、零运行时开销、编译时检查
│   ├── 缺点: 需要重新编译、不支持热插拔
│   └── 适用: 编译时扩展、内置插件
│
├── 方案 2: 运行时动态加载 (libloading)
│   ├── 原理: 运行时加载 .so/.dll 文件,查找符号
│   ├── 优点: 热插拔、无需重新编译主程序
│   ├── 缺点: 不安全 (unsafe)、ABI 兼容性问题
│   └── 适用: 第三方插件、热更新
│
└── 方案 3: C 兼容动态库 (dlopen + cdylib)
    ├── 原理: 使用 C ABI 导出函数,动态加载
    ├── 优点: 跨语言兼容、标准方式
    ├── 缺点: 需要手动管理内存、unsafe
    └── 适用: 跨语言插件、C/C++ 兼容

方案 1: 编译时插件注册 (inventory)

原理inventory crate 利用 Rust 的链接器特性,在编译时收集所有通过 submit! 宏提交的插件。

核心概念

┌─────────────────────────────────────────────────────────────┐
│                    编译时插件注册流程                         │
│                                                              │
│  main.rs              plugin_add.rs          plugin_rm.rs   │
│  ┌─────────┐          ┌───────────┐          ┌───────────┐  │
│  │collect! │          │submit!    │          │submit!    │  │
│  │         │          │           │          │           │  │
│  │ 收集所有 │◄─────────┤ 注册 add  │          │ 注册 remove│  │
│  │ 插件    │          │ 插件      │          │ 插件      │  │
│  │         │          │           │          │           │  │
│  └────┬────┘          └───────────┘          └───────────┘  │
│       │                                                       │
│       ▼                                                       │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  运行时: inventory::iter::<Plugin> 遍历所有已注册插件     │ │
│  └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

完整示例(来自项目代码):

use inventory::submit;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

// 定义插件 trait
trait InventoryOp: Send + Sync {
    fn name(&self) -> &'static str;
    fn execute(&self, inventory: &Mutex<HashMap<String, u32>>, item: &str, quantity: u32);
}

// 定义插件注册结构体
#[derive(Clone, Copy)]
struct InventoryPlugin {
    name: &'static str,
    handler: &'static dyn InventoryOp,
}

// 收集所有提交的插件
inventory::collect!(InventoryPlugin);

// 插件 1: 添加物品
struct AddItem;
impl InventoryOp for AddItem {
    fn name(&self) -> &'static str { "add" }
    fn execute(&self, inventory: &Mutex<HashMap<String, u32>>, item: &str, quantity: u32) {
        let mut inv = inventory.lock().unwrap();
        let current = inv.get(item).copied().unwrap_or(0);
        inv.insert(item.to_string(), current + quantity);
        println!("Added {} {} to inventory", quantity, item);
    }
}

inventory::submit! {
    InventoryPlugin { name: "add", handler: &AddItem }
}

// 插件 2: 移除物品
struct RemoveItem;
impl InventoryOp for RemoveItem {
    fn name(&self) -> &'static str { "remove" }
    fn execute(&self, inventory: &Mutex<HashMap<String, u32>>, item: &str, quantity: u32) {
        let mut inv = inventory.lock().unwrap();
        if let Some(current) = inv.get_mut(item) {
            if *current >= quantity {
                *current -= quantity;
                println!("Removed {} {} from inventory", quantity, item);
            } else {
                println!("Error: Not enough {} in inventory", item);
            }
        } else {
            println!("Error: {} not found in inventory", item);
        }
    }
}

inventory::submit! {
    InventoryPlugin { name: "remove", handler: &RemoveItem }
}

fn inventory_main() {
    let inventory = Arc::new(Mutex::new(HashMap::new()));
    inventory.lock().unwrap().insert("apple".to_string(), 10);
    
    // 收集所有插件
    let mut plugins: HashMap<&str, &dyn InventoryOp> = HashMap::new();
    for plugin in inventory::iter::<InventoryPlugin> {
        if !plugin.name.is_empty() {
            plugins.insert(plugin.name, plugin.handler);
        }
    }
    
    // 执行操作
    let ops = vec![
        ("add", "apple", 5),
        ("remove", "apple", 3),
        ("add", "sword", 2),
    ];
    
    for (op_name, item, quantity) in ops {
        if let Some(op) = plugins.get(op_name) {
            op.execute(&inventory, item, quantity);
        }
    }
}

方案 2: 运行时动态加载 (libloading)

原理:运行时加载 .so(Linux)、.dylib(macOS)、.dll(Windows)文件,通过符号名查找函数。

use libloading::{Library, Symbol};

// 定义插件函数签名
type PluginInit = unsafe extern "C" fn() -> *mut std::os::raw::c_void;
type PluginExecute = unsafe extern "C" fn(*mut std::os::raw::c_void);

fn load_plugin(path: &str) -> Result<(), Box<dyn std::error::Error>> {
    unsafe {
        // 加载动态库
        let lib = Library::new(path)?;
        
        // 查找符号
        let init: Symbol<PluginInit> = lib.get(b"plugin_init")?;
        let execute: Symbol<PluginExecute> = lib.get(b"plugin_execute")?;
        
        // 初始化插件
        let handle = init();
        
        // 执行插件
        execute(handle);
        
        Ok(())
    }
}

插件端代码(需要编译为 cdylib):

// Cargo.toml: [lib] crate-type = ["cdylib"]

#[no_mangle]
pub extern "C" fn plugin_init() -> *mut std::os::raw::c_void {
    println!("Plugin initialized");
    std::ptr::null_mut()
}

#[no_mangle]
pub extern "C" fn plugin_execute(_handle: *mut std::os::raw::c_void) {
    println!("Plugin executed");
}

方案 3: C 兼容动态库 (dlopen)

原理:使用 dlopen 加载 C 兼容的动态库,适合跨语言插件。

use dlopen::symbor::Library;
use dlopen_derive::StructSymbols;

#[derive(StructSymbols)]
struct PluginApi {
    init: extern "C" fn(),
    execute: extern "C" fn(),
}

fn load_plugin_dlopen(path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let lib = Library::open(path)?;
    let api: PluginApi = unsafe { lib.symbols() }?;
    
    (api.init)();
    (api.execute)();
    
    Ok(())
}

Rust 插件系统设计思想

1. 编译时 vs 运行时

特性编译时 (inventory)运行时 (libloading)
类型安全✅ 完全安全❌ 需要 unsafe
热插拔❌ 需要重新编译✅ 支持
性能✅ 零开销⚠️ 动态查找
跨语言❌ 仅 Rust✅ C ABI 兼容
调试✅ 容易⚠️ 困难

2. 插件注册模式

┌─────────────────────────────────────────────────────────────┐
│                        插件注册模式                          │
│                                                              │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐               │
│  │ Plugin A │    │ Plugin B │    │ Plugin C │               │
│  └────┬─────┘    └────┬─────┘    └────┬─────┘               │
│       │               │               │                      │
│       ▼               ▼               ▼                      │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │                    Plugin Registry                       │ │
│  │  ┌─────────┐  ┌─────────┐  ┌─────────┐                 │ │
│  │  │ Plugin A│  │ Plugin B│  │ Plugin C│                 │ │
│  │  └─────────┘  └─────────┘  └─────────┘                 │ │
│  └────────────────────────┬────────────────────────────────┘ │
│                           │                                  │
│                           ▼                                  │
│                  ┌─────────────────┐                         │
│                  │   Host App      │                         │
│                  │  (主应用程序)    │                         │
│                  └─────────────────┘                         │
└─────────────────────────────────────────────────────────────┘

3. 插件生命周期

加载 → 初始化 → 注册 → 执行 → 卸载
  │        │        │        │        │
  ▼        ▼        ▼        ▼        ▼
load    init    register  execute  unload

Rust 插件框架生态

框架类型特点适用场景
inventory编译时类型安全、零开销内置插件、编译时扩展
extism运行时WASM 插件、沙箱安全第三方插件、安全隔离
libloading运行时原生动态库热插拔、C 兼容
dlopen运行时C ABI 兼容跨语言插件
pluginator编译时宏驱动简单插件系统
wasmer运行时WASM 运行时安全沙箱插件

Extism (WASM 插件)

Extism 是目前 Rust 生态最流行的 WASM 插件框架:

use extism::{Manifest, Plugin, Wasm};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 加载 WASM 插件
    let manifest = Manifest::new([Wasm::file("plugin.wasm")]);
    let mut plugin = Plugin::new(&manifest, [], true)?;
    
    // 调用插件函数
    let res = plugin.call("greet", "World")?;
    println!("Plugin response: {}", res);
    
    Ok(())
}

优势

  • 沙箱安全(WASM 隔离)
  • 跨语言(任何能编译 WASM 的语言)
  • 热插拔
  • 资源限制

常见错误

错误 1: inventory 插件未注册

// ❌ 错误:忘记调用 collect!
// inventory::collect!(InventoryPlugin);  // 缺少这行!

// ✅ 正确:必须先 collect
inventory::collect!(InventoryPlugin);

错误 2: libloading 符号查找失败

// ❌ 错误:符号名不匹配
let func: Symbol<FnType> = lib.get(b"wrong_name")?;

// ✅ 正确:使用 #[no_mangle] 确保符号名一致
#[no_mangle]
pub extern "C" fn plugin_init() { ... }

错误 3: 动态库 ABI 不兼容

// ❌ 错误:使用 Rust ABI(不稳定)
pub fn plugin_init() { ... }

// ✅ 正确:使用 C ABI
#[no_mangle]
pub extern "C" fn plugin_init() { ... }

动手练习

练习 1: 添加新插件

在 inventory 示例中添加一个 "clear" 操作插件,清空所有物品:

// TODO: 实现 ClearAll 插件
// 1. 定义 ClearAll 结构体
// 2. 实现 InventoryOp trait
// 3. 使用 inventory::submit! 注册
点击查看答案
struct ClearAll;
impl InventoryOp for ClearAll {
    fn name(&self) -> &'static str { "clear" }
    fn execute(&self, inventory: &Mutex<HashMap<String, u32>>, _item: &str, _quantity: u32) {
        let mut inv = inventory.lock().unwrap();
        inv.clear();
        println!("Cleared all items from inventory");
    }
}

inventory::submit! {
    InventoryPlugin { name: "clear", handler: &ClearAll }
}

练习 2: 设计插件接口

为一个日志系统设计插件接口,支持不同的日志输出方式(控制台、文件、网络):

// TODO: 定义 LoggerPlugin trait
// TODO: 定义插件注册结构体
// TODO: 实现 ConsoleLogger 插件
点击查看答案
use inventory::submit;

trait LoggerPlugin: Send + Sync {
    fn name(&self) -> &'static str;
    fn log(&self, level: &str, message: &str);
}

#[derive(Clone, Copy)]
struct LoggerRegistry {
    name: &'static str,
    handler: &'static dyn LoggerPlugin,
}

inventory::collect!(LoggerRegistry);

struct ConsoleLogger;
impl LoggerPlugin for ConsoleLogger {
    fn name(&self) -> &'static str { "console" }
    fn log(&self, level: &str, message: &str) {
        println!("[{}] {}", level, message);
    }
}

inventory::submit! {
    LoggerRegistry { name: "console", handler: &ConsoleLogger }
}

小结

核心要点

  1. 编译时插件 (inventory) - 类型安全、零开销,适合内置扩展
  2. 运行时插件 (libloading) - 热插拔、灵活,需要 unsafe
  3. WASM 插件 (extism) - 沙箱安全、跨语言,推荐用于第三方插件
  4. 设计原则 - 定义清晰的 trait 接口、使用注册模式管理插件

关键术语

English中文说明
Plugin插件可扩展的功能模块
Registry注册表管理所有已注册插件
Hot Reload热重载运行时加载/卸载插件
WASMWebAssembly安全的沙箱执行环境
ABI应用二进制接口跨语言兼容的接口规范
cdylibC 动态库编译为 C 兼容的动态库

下一步


术语表

English中文
Plugin System插件系统
Registry注册表
Hot Reload热重载
Dynamic Library动态库
Symbol符号
ABI应用二进制接口
WASMWebAssembly
Sandbox沙箱
cdylibC 动态库
rlibRust 静态库

完整示例:crates/awesome/src/services/inventory_sample.rs


继续学习

💡 记住:Rust 的插件系统设计核心是"编译时安全 + 运行时灵活"。根据需求选择 inventory(编译时)、libloading(运行时)、或 extism(WASM 沙箱)!

序列 ID 生成

开篇故事

想象你在设计一个电商平台。每当有新订单产生,系统需要为订单分配一个唯一的标识符。如果两个订单获得相同的 ID,后果可能是客户 A 支付了客户 B 的订单、库存系统混乱、财务报表数据错误。

在分布式系统中,这个问题更加棘手。多个服务器同时处理请求,如何保证生成的 ID 在全局范围内唯一?这就是 UUID (Universally Unique Identifier) 的价值所在。


本章适合谁

如果你需要为数据库记录生成唯一主键、在分布式系统中标识资源、实现订单号或流水号等业务 ID,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 使用 uuid crate 生成不同版本的 UUID (v3, v4, v5, v7)
  2. 根据业务场景选择合适的 UUID 版本
  3. 基于业务字段生成确定性的 GUID
  4. 解析和格式化 UUID 字符串
  5. 使用 URN 格式表示 UUID

前置要求

  • Rust 基础语法
  • 理解字符串和字节切片
  • 熟悉 Cargo 依赖管理

依赖安装

运行以下命令安装所需依赖:

cargo add uuid --features v4,v5,v7,md5
cargo add md-5
cargo add chrono

第一个例子

最常用的 UUID v4 随机生成:

use uuid::Uuid;

fn main() {
    let id = Uuid::new_v4();
    println!("生成的 UUID: {}", id);
    // 输出示例: 550e8400-e29b-41d4-a716-446655440000
}

完整示例:crates/awesome/src/sequences/uuid_sample.rs


原理解析

UUID 版本对比

版本生成方式特点适用场景
v1时间戳+MAC 地址可排序,暴露硬件遗留系统兼容
v3MD5 哈希确定性,已弃用旧系统兼容
v4随机数最常用,简单通用唯一标识
v5SHA-1 哈希确定性,安全相同输入相同输出
v7时间戳+随机可排序,现代标准数据库主键
NIL全零特殊值表示"空"或"无效"

UUID v4 - 随机生成

// 最常用的 UUID 版本,使用加密安全的随机数生成器
let uuid_v4 = Uuid::new_v4();
println!("Version 4 UUID: {}", uuid_v4);
// 输出: 936c342f-76a0-4a8b-8e1d-3b7c8a9e0f1d

UUID v3/v5 - 基于名称的确定性生成

// 使用命名空间和名称生成确定性 UUID
// 相同输入总是产生相同输出
let namespace_url = Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8")?;
let name = "example.com";

// v3 使用 MD5(已弃用,仅用于兼容)
let uuid_v3 = Uuid::new_v3(&namespace_url, name.as_bytes());
println!("Version 3 UUID: {}", uuid_v3);

// v5 使用 SHA-1(推荐用于确定性场景)
let uuid_v5 = Uuid::new_v5(&namespace_url, name.as_bytes());
println!("Version 5 UUID: {}", uuid_v5);

UUID v7 - 时间排序(现代标准)

// 最简单的 v7 生成方式
let uuid_v7 = Uuid::now_v7();
println!("UUID v7: {}", uuid_v7);

// 使用自定义时间戳
let now = SystemTime::now()
    .duration_since(SystemTime::UNIX_EPOCH)?;

let ts = Timestamp::from_unix(NoContext, now.as_secs(), now.subsec_nanos());
let uuid_v7_custom = Uuid::new_v7(ts);
println!("UUID v7 (custom timestamp): {}", uuid_v7_custom);

优势

  • 按时间排序,适合数据库索引
  • 无需暴露 MAC 地址
  • 比 v4 更适合做主键(索引友好)

NIL UUID - 特殊值

// 全零 UUID,表示"空"或"无效"
let uuid_nil = Uuid::nil();
println!("NIL UUID: {}", uuid_nil);
// 输出: 00000000-0000-0000-0000-000000000000

URN 格式和解析

// URN (Uniform Resource Name) 格式
let uuid = Uuid::new_v4();
println!("URN: {}", uuid.urn());
// 输出: urn:uuid:936c342f-76a0-4a8b-8e1d-3b7c8a9e0f1d

// 从字符串解析 UUID
let uuid_str = "f8a7e0d1-c2b3-4a5b-6c7d-8e9f0a1b2c3d";
match Uuid::parse_str(uuid_str) {
    Ok(parsed) => println!("Parsed: {}", parsed),
    Err(e) => println!("Parse failed: {}", e),
}

基于业务字段生成 GUID

/// 基于多个业务字段生成确定性 GUID
pub fn generate_guid_from_fields(
    tenant_id: &str,
    area_id: u64,
    area_code: &str,
    object_code: &str,
    object_type: i32,
) -> Uuid {
    // 组合所有业务字段
    let combined = format!(
        "{}-{}-{}-{}-{}",
        tenant_id, area_id, area_code, object_code, object_type
    );

    // 使用 MD5 哈希生成 16 字节
    let mut hasher = md5::Context::new();
    hasher.write(combined.as_bytes());
    let md5_bytes = hasher.finalize().into();

    // 从 MD5 字节创建 UUID
    Uuid::from_bytes(md5_bytes)
}

// 使用示例
let guid = generate_guid_from_fields("T001", 1001, "A01", "OBJ-001", 0);
println!("Generated GUID: {}", guid);

常见错误

错误 1: 混淆 UUID 字符串格式

使用标准 36 字符格式(含连字符):550e8400-e29b-41d4-a716-446655440000

错误 2: 假设 UUID 完全唯一

关键系统应检查唯一性(v4 碰撞概率极低但非零)

错误 3: 在数据库主键中使用 v4 而非 v7

v4 随机 UUID 不适合做聚簇索引(插入位置随机导致页分裂),v7 时间排序更适合


知识检查

问题 1: 哪个 UUID 版本使用随机数生成?

问题 2: 数据库主键应该选择哪个 UUID 版本?

问题 3: UUID v3 和 v5 的区别是什么?

点击查看答案与解析
  1. UUID v4 使用加密安全的随机数生成器
  2. UUID v7(时间排序,索引友好,避免页分裂)
  3. v3 使用 MD5(已弃用),v5 使用 SHA-1(推荐)

关键理解: 选择 UUID 版本取决于你的需求:唯一性用 v4,排序用 v7,确定性用 v5。


延伸阅读

学习完序列 ID 生成后,你可能还想了解:

  • Snowflake ID - Twitter 的分布式 ID 生成算法
  • ULID - 类似 UUID v7 的可排序 ID
  • NanoID - 紧凑的 URL 安全 ID

选择建议:

  • 通用唯一标识 → UUID v4
  • 数据库主键 → UUID v7 或 Snowflake
  • 确定性 ID → UUID v5 或 ULID

小结

核心要点:UUID v4 随机生成、UUID v7 时间排序、UUID v5 确定性生成、NIL 表示空值

完整示例:crates/awesome/src/sequences/uuid_sample.rs

消息队列 (Message Queue)

开篇故事

想象你经营着一家繁忙的餐厅。传统的方式是顾客直接告诉厨师要吃什么——但当顾客太多时,厨房会陷入混乱,订单被遗忘或搞混。消息队列就像是餐厅的前台接待系统:顾客(生产者)把订单放到柜台(队列),厨师(消费者)按顺序从柜台取单并制作。这样即使高峰期也能有序运作。

在分布式系统中,消息队列是服务间异步通信的核心组件。MQTT(Message Queuing Telemetry Transport)是一种轻量级的发布/订阅消息协议,特别适合物联网场景——从智能家居到工业传感器,MQTT 都能稳定可靠地传递消息。


本章适合谁

如果你已经理解 Rust 基础编程和异步编程,现在想学习:

  • 如何在微服务架构中使用消息队列进行异步通信
  • MQTT 协议的发布/订阅模式及其实现
  • 如何使用 rumqttc 客户端库连接 MQTT Broker

本章适合你。消息队列是构建可扩展、松耦合系统的关键技术。


你会学到什么

完成本章后,你可以:

  1. 解释消息队列的核心概念和应用场景
  2. 理解 MQTT 协议的发布/订阅模式
  3. 使用 rumqttc 创建同步和异步 MQTT 客户端
  4. 实现消息的发布(Publish)和订阅(Subscribe)
  5. 理解 QoS 等级及其对消息可靠性的影响
  6. 编写基于 MQTT 的物联网通信程序
  7. 处理连接保持和重连逻辑

前置要求

学习本章前,你需要理解:

  • 所有权 - 理解所有权和生命周期
  • 异步编程 - 理解 async/await 基础
  • Tokio - 使用 Tokio 异步运行时
  • 安装 MQTT Broker(推荐 Mosquitto 或 EMQX)用于测试

依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add rumqttc
cargo add serde --features derive
cargo add serde_json
cargo add tracing
cargo add tracing-subscriber

第一个例子

让我们看一个最简单的 MQTT 同步客户端示例:

use rumqttc::{Client, MqttOptions, QoS};
use std::time::Duration;

fn main() {
    // 配置 MQTT 连接选项
    let mut mqttoptions = MqttOptions::new("client-001", "127.0.0.1", 1883);
    mqttoptions.set_keep_alive(Duration::from_secs(5));

    // 创建客户端和连接
    let (mut client, mut connection) = Client::new(mqttoptions, 20);

    // 订阅主题
    client.subscribe("hello/rumqtt", QoS::AtMostOnce).unwrap();
    println!("已订阅主题 'hello/rumqtt'");

    // 在独立线程中发布消息
    std::thread::spawn(move || {
        for i in 0..10 {
            client.publish("hello/rumqtt", QoS::AtLeastOnce, false, vec![i; i as usize]).unwrap();
            println!("发送消息 {}", i);
            std::thread::sleep(Duration::from_millis(500));
        }
    });

    // 接收消息
    for notification in connection.iter() {
        println!("收到通知: {:?}", notification);
    }
}

发生了什么?

  1. MqttOptions 配置 MQTT Broker 地址和客户端 ID
  2. Client::new() 创建同步客户端,返回 (client, connection) 元组
  3. subscribe() 订阅指定主题,准备接收消息
  4. publish() 向主题发送消息,所有订阅者都会收到
  5. connection.iter() 阻塞等待并处理入站消息

完整示例:crates/awesome/src/mq/rumqtt_sample.rs


原理解析

MQTT 架构概览

┌─────────────────────────────────────────────────────────────────────┐
│                        MQTT 发布/订阅架构                             │
│                                                                      │
│   ┌──────────────┐         ┌──────────────┐         ┌──────────────┐│
│   │  Publisher   │         │              │         │  Subscriber  ││
│   │  (发布者)     │────────→│ MQTT Broker  │←────────│  (订阅者)     ││
│   │              │ publish │  (消息代理)   │ forward │              ││
│   └──────────────┘         └──────┬───────┘         └──────────────┘│
│                                   │                                  │
│   ┌──────────────┐         ┌──────┴───────┐         ┌──────────────┐│
│   │  Publisher   │         │              │         │  Subscriber  ││
│   │  (传感器)     │────────→│              │←────────│  (控制器)     ││
│   └──────────────┘         └──────────────┘         └──────────────┘│
│                                                                      │
│   主题层级:                                                            │
│   home/                                                               │
│   ├── bedroom/                                                        │
│   │   ├── temperature  ← 传感器发布到这里                               │
│   │   └── humidity                                                    │
│   └── livingroom/                                                     │
│       ├── light          ← 控制器订阅这里                               │
│       └── temperature                                                 │
│                                                                      │
│   通配符: home/+/temperature  匹配所有房间温度                          │
│          home/bedroom/#      匹配卧室所有子主题                         │
└─────────────────────────────────────────────────────────────────────┘

消息生命周期(QoS 1 流程)

Publisher                           Broker                          Subscriber
   │                                   │                               │
   │  1. CONNECT                       │                               │
   │ ────────────────────────────────→ │                               │
   │                                   │  2. CONNACK                   │
   │ ←──────────────────────────────── │                               │
   │                                   │                               │
   │  3. SUBSCRIBE (由订阅者发送)       │                               │
   │ ────────────────────────────────→ │                               │
   │                                   │  4. SUBACK                    │
   │ ←──────────────────────────────── │                               │
   │                                   │                               │
   │  5. PUBLISH (QoS 1)               │                               │
   │ ────────────────────────────────→ │                               │
   │                                   │  6. 匹配主题                   │
   │                                   │ ────────────────────────────→ │
   │                                   │                               │
   │                                   │  7. PUBLISH (转发)             │
   │                                   │ ←──────────────────────────── │
   │                                   │                               │
   │                                   │  8. PUBACK                    │
   │                                   │ ────────────────────────────→ │
   │  9. PUBACK                        │                               │
   │ ←──────────────────────────────── │                               │
   │                                   │                               │

核心概念

1. MQTT Broker(消息代理)

MQTT Broker 是消息的中转站,负责接收发布者的消息并转发给订阅者:

// 连接到本地 MQTT Broker
let mut mqttoptions = MqttOptions::new("client-id", "127.0.0.1", 1883);

// 或连接到公共测试 Broker
let mut mqttoptions = MqttOptions::new("client-id", "test.mosquitto.org", 1883);

mqttoptions.set_keep_alive(Duration::from_secs(5));

2. Topic(主题)

主题是消息的地址,使用层级结构:

// 简单主题
let topic = "sensors/temperature";

// 多层主题
let topic = "home/bedroom/temperature";

// 使用通配符订阅
client.subscribe("sensors/+/temperature", QoS::AtMostOnce).unwrap(); // + 匹配一级
client.subscribe("home/#", QoS::AtMostOnce).unwrap(); // # 匹配多级

3. QoS(服务质量等级)

QoS 等级名称说明应用场景
0AtMostOnce最多一次,不确认高频数据,丢失可接受
1AtLeastOnce至少一次,需确认关键数据,允许重复
2ExactlyOnce恰好一次,四次握手关键命令,不可重复
use rumqttc::QoS;

// QoS 0: 发送即忘
client.publish("sensor/data", QoS::AtMostOnce, false, payload).unwrap();

// QoS 1: 确保送达
client.publish("device/command", QoS::AtLeastOnce, false, payload).unwrap();

// QoS 2: 确保仅送达一次(开销最大)
client.publish("payment/confirm", QoS::ExactlyOnce, false, payload).unwrap();

异步客户端架构

┌──────────────────────────────────────────────────────────────────┐
│                        Tokio Runtime                              │
│                                                                   │
│  ┌──────────────┐        ┌──────────────┐                        │
│  │  Publisher   │        │  EventLoop   │                        │
│  │   Task       │        │   Task       │                        │
│  │              │        │              │                        │
│  │  client.     │───────→│ eventloop.   │←───────┐               │
│  │  publish()   │  send  │ poll().await │        │               │
│  │              │        │              │        │               │
│  └──────────────┘        └──────┬───────┘        │               │
│                                 │                │               │
│                                 │  TCP/TLS       │               │
│                                 ▼                │               │
│                        ┌──────────────┐          │               │
│                        │ MQTT Broker  │──────────┘               │
│                        │  127.0.0.1   │  publish msg             │
│                        └──────────────┘                          │
└──────────────────────────────────────────────────────────────────┘

异步客户端完整示例

use rumqttc::{AsyncClient, Event, MqttOptions, Packet, QoS};

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("async-client", "127.0.0.1", 1883);
    let (mut client, mut eventloop) = AsyncClient::new(mqttoptions, 15);

    // 异步订阅
    client.subscribe("hello/rumqtt", QoS::AtMostOnce).await.unwrap();

    // 异步发布
    tokio::spawn(async move {
        for i in 0..10 {
            client.publish("hello/rumqtt", QoS::AtLeastOnce, false, vec![i]).await.unwrap();
            tokio::time::sleep(Duration::from_secs(3)).await;
        }
    });

    // 异步处理事件
    loop {
        match eventloop.poll().await {
            Ok(Event::Incoming(Packet::Publish(p))) => {
                println!("收到消息: {:?}", p.payload);
            }
            Err(e) => {
                eprintln!("错误: {:?}", e);
                break;
            }
            _ => {}
        }
    }
}

MQTT 连接状态机

                  ┌─────────────┐
                  │  Disconnected│
                  └──────┬──────┘
                         │ CONNECT
                         ▼
                  ┌─────────────┐
            ┌─────│  Connecting  │─────┐
            │     └──────┬──────┘     │
            │            │ CONNACK    │
            │            ▼            │
            │     ┌─────────────┐     │
            │     │  Connected   │     │
            │     └──────┬──────┘     │
            │            │            │
            │     ┌──────┴──────┐     │
            │     │             │     │
            ▼     ▼             │     ▼
      ┌──────────┐      ┌──────────┐  ┌──────────┐
      │Publishing│      │Subscribing│  │Pinging   │
      └────┬─────┘      └────┬─────┘  └────┬─────┘
           │                 │              │
           └─────────────────┴──────────────┘
                         │
                         │ DISCONNECT / Error
                         ▼
                  ┌─────────────┐
                  │  Disconnected│
                  └─────────────┘

常见错误

错误 1: 忘记处理连接保持

// ❌ 错误:没有设置 keep_alive
let mqttoptions = MqttOptions::new("client", "broker", 1883);

// ✅ 正确:设置 keep_alive 保持连接
let mut mqttoptions = MqttOptions::new("client", "broker", 1883);
mqttoptions.set_keep_alive(Duration::from_secs(5));

问题:没有 keep_alive,Broker 会在空闲后断开连接。

错误 2: 同步客户端在异步上下文中使用

// ❌ 错误:在 async 函数中使用同步 Client
async fn bad_example() {
    let (client, _) = Client::new(mqttoptions, 10); // 阻塞!
    client.subscribe("topic", QoS::AtMostOnce).unwrap(); // 阻塞!
}

// ✅ 正确:使用 AsyncClient
async fn good_example() {
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    client.subscribe("topic", QoS::AtMostOnce).await.unwrap();
}

错误 3: 没有处理连接断开和重连

// ❌ 错误:连接断开时直接退出
loop {
    let notification = connection.iter().next().unwrap(); // 断开时 panic
}

// ✅ 正确:优雅处理连接错误
loop {
    match eventloop.poll().await {
        Ok(event) => handle_event(event),
        Err(e) => {
            eprintln!("连接错误: {:?}, 尝试重连...", e);
            tokio::time::sleep(Duration::from_secs(5)).await;
            // 重连逻辑
        }
    }
}

错误 4: 主题名称包含非法字符

// ❌ 错误:主题包含空格和特殊字符
let topic = "my topic with spaces";

// ✅ 正确:使用合法字符
let topic = "my/topic/with/hierarchy";

// 合法字符:字母、数字、下划线、正斜杠
// 正斜杠用于层级分隔

动手练习

练习 1: 修复订阅示例

下面的代码有什么问题?

fn main() {
    let mqttoptions = MqttOptions::new("client", "127.0.0.1", 1883);
    let (client, mut connection) = Client::new(mqttoptions, 10);
    
    // 订阅主题
    client.subscribe("test/topic", QoS::AtMostOnce).unwrap();
    
    // 发送消息
    client.publish("test/topic", QoS::AtLeastOnce, false, vec![1, 2, 3]).unwrap();
    
    // 接收消息
    while let Some(notification) = connection.iter().next() {
        println!("{:?}", notification);
    }
}
点击查看答案与解析

问题

  1. 没有设置 keep_alive,连接可能被 Broker 断开
  2. connection.iter() 返回 Result,需要正确处理
  3. 发送和接收在同一线程,没有并发处理

修复方案

use rumqttc::{Client, MqttOptions, QoS, Packet, Event};
use std::time::Duration;
use std::thread;

fn main() {
    let mut mqttoptions = MqttOptions::new("client", "127.0.0.1", 1883);
    mqttoptions.set_keep_alive(Duration::from_secs(5)); // ✅ 保持连接
    
    let (mut client, mut connection) = Client::new(mqttoptions, 10);
    
    // 订阅主题
    client.subscribe("test/topic", QoS::AtMostOnce).unwrap();
    
    // 在独立线程发送消息 ✅
    thread::spawn(move || {
        for i in 0..5 {
            client.publish("test/topic", QoS::AtLeastOnce, false, vec![i]).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });
    
    // 接收消息
    for notification in connection.iter() {
        match notification {
            Ok(Event::Incoming(Packet::Publish(p))) => {
                println!("收到消息: {:?}", p.payload);
            }
            Err(e) => eprintln!("错误: {:?}", e), // ✅ 错误处理
            _ => {}
        }
    }
}

练习 2: 实现温度监控器

补全下面的代码,实现一个简单的温度监控器:

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("temp-monitor", "127.0.0.1", 1883);
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    
    // TODO: 订阅温度主题 "home/+/temperature"
    // TODO: 当温度超过 30 度时打印警告
    
    loop {
        match eventloop.poll().await {
            // TODO: 处理收到的消息
            _ => {}
        }
    }
}
点击查看答案
use rumqttc::{AsyncClient, MqttOptions, QoS, Packet, Event};

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("temp-monitor", "127.0.0.1", 1883);
    mqttoptions.set_keep_alive(Duration::from_secs(5));
    
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    
    // 订阅所有房间的温度
    client.subscribe("home/+/temperature", QoS::AtMostOnce).await.unwrap();
    println!("已订阅温度主题");
    
    loop {
        match eventloop.poll().await {
            Ok(Event::Incoming(Packet::Publish(p))) => {
                // 解析温度值
                if let Ok(temp_str) = String::from_utf8(p.payload.to_vec()) {
                    if let Ok(temp) = temp_str.parse::<f32>() {
                        println!("[{}] 温度: {:.1}°C", p.topic, temp);
                        
                        // 超过 30 度报警
                        if temp > 30.0 {
                            println!("⚠️ 警告:{} 温度过高!", p.topic);
                        }
                    }
                }
            }
            Ok(Event::Incoming(Packet::ConnAck(_))) => {
                println!("已连接到 Broker");
            }
            Err(e) => {
                eprintln!("连接错误: {:?}", e);
                tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
            }
            _ => {}
        }
    }
}

练习 3: 理解 QoS 等级

预测以下代码中消息的传输可靠性(假设网络不稳定):

// 传感器 A:QoS 0
client.publish("sensor/a", QoS::AtMostOnce, false, vec![1]).await.unwrap();

// 传感器 B:QoS 1  
client.publish("sensor/b", QoS::AtLeastOnce, false, vec![2]).await.unwrap();

// 传感器 C:QoS 2
client.publish("sensor/c", QoS::ExactlyOnce, false, vec![3]).await.unwrap();

如果网络突然中断,哪些消息可能丢失?

点击查看解析

结果

传感器QoS可能丢失?原因
A0✅ 可能丢失发送即忘,无确认
B1❌ 不会丢失需要 ACK,会重试
C2❌ 不会丢失四次握手确保送达

关键点

  • QoS 0:性能最好,但不可靠
  • QoS 1:可靠性+性能平衡,可能重复
  • QoS 2:最高可靠性,但开销最大

适用场景

  • QoS 0:高频传感器数据(每秒上报)
  • QoS 1:设备控制命令(开关灯)
  • QoS 2:支付、关键配置更新

实际应用

应用场景 1: 智能家居控制中心

use rumqttc::{AsyncClient, MqttOptions, QoS, Packet, Event};
use std::collections::HashMap;
use tokio::sync::RwLock;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("home-controller", "127.0.0.1", 1883);
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    
    // 存储设备状态
    let devices: Arc<RwLock<HashMap<String, String>>> = Arc::new(RwLock::new(HashMap::new()));
    
    // 订阅所有设备主题
    client.subscribe("home/+/status", QoS::AtMostOnce).await.unwrap();
    client.subscribe("home/+/command", QoS::AtLeastOnce).await.unwrap();
    
    loop {
        match eventloop.poll().await {
            Ok(Event::Incoming(Packet::Publish(p))) => {
                let topic = p.topic;
                let payload = String::from_utf8_lossy(&p.payload);
                
                if topic.contains("/status") {
                    // 更新设备状态
                    let mut dev = devices.write().await;
                    dev.insert(topic.clone(), payload.to_string());
                    println!("设备 [{}] 状态: {}", topic, payload);
                }
            }
            Err(e) => {
                eprintln!("MQTT 错误: {:?}", e);
            }
            _ => {}
        }
    }
}

应用场景 2: 传感器数据采集

use rumqttc::{AsyncClient, MqttOptions, QoS};
use tokio::time::{interval, Duration};
use rand::Rng;

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("sensor-sim", "127.0.0.1", 1883);
    let (client, _) = AsyncClient::new(mqttoptions, 10);
    
    let mut ticker = interval(Duration::from_secs(5));
    let mut rng = rand::thread_rng();
    
    loop {
        ticker.tick().await;
        
        // 模拟传感器数据
        let temperature = 20.0 + rng.gen::<f32>() * 10.0;
        let humidity = 40.0 + rng.gen::<f32>() * 30.0;
        
        // 发布温度(QoS 0,高频数据)
        client.publish(
            "factory/sensor1/temperature",
            QoS::AtMostOnce,
            false,
            format!("{:.2}", temperature).into_bytes()
        ).await.unwrap();
        
        // 发布湿度(QoS 0)
        client.publish(
            "factory/sensor1/humidity",
            QoS::AtMostOnce,
            false,
            format!("{:.2}", humidity).into_bytes()
        ).await.unwrap();
        
        println!("已发布: T={:.1}°C, H={:.1}%", temperature, humidity);
    }
}

应用场景 3: 带遗嘱消息的客户端

use rumqttc::{AsyncClient, LastWill, MqttOptions, QoS};

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("device-001", "127.0.0.1", 1883);
    
    // 设置遗嘱消息:意外断开时自动发布
    let will = LastWill::new(
        "device/001/status",
        b"offline",
        QoS::AtLeastOnce,
        true // retained 消息
    );
    mqttoptions.set_last_will(will);
    
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    
    // 上线时发送状态
    client.publish(
        "device/001/status",
        QoS::AtLeastOnce,
        true, // retained
        b"online"
    ).await.unwrap();
    
    println!("设备已上线");
    
    // 保持连接
    loop {
        match eventloop.poll().await {
            Err(e) => {
                eprintln!("连接断开: {:?}", e);
                break;
            }
            _ => {}
        }
    }
}

故障排查 (FAQ)

Q: 连接失败 "Connection refused" 怎么办?

A: 检查以下几点:

  1. Broker 是否运行

    # 测试 Broker 连通性
    mosquitto_pub -t test -m "hello" -h 127.0.0.1
    
  2. 端口是否正确

    • MQTT 默认端口:1883
    • MQTT over TLS:8883
    • WebSocket:8083
  3. 防火墙设置

    # 开放端口(Linux)
    sudo ufw allow 1883
    

Q: 什么时候用同步客户端,什么时候用异步客户端?

A:

场景推荐客户端原因
简单脚本、命令行工具Client简单直接
需要与 Tokio 集成AsyncClient兼容性
高性能服务器AsyncClient非阻塞,高吞吐
嵌入式/资源受限Client更少的依赖

Q: 如何处理消息积压?

A: 使用背压控制和合理配置:

// 增大接收缓冲区
let (client, eventloop) = AsyncClient::new(mqttoptions, 100); // 缓冲区 100

// 使用多消费者处理
for _ in 0..4 {
    let rx = rx.clone();
    tokio::spawn(async move {
        while let Some(msg) = rx.recv().await {
            process_message(msg).await;
        }
    });
}

Q: MQTT vs Kafka vs RabbitMQ 如何选择?

A:

特性MQTTKafkaRabbitMQ
协议复杂度简单中等复杂
适用场景IoT、移动端大数据流企业消息
消息持久化可选强制可选
QoS 支持内置需配置需配置
资源占用极低中等
典型部署边缘设备数据中心企业服务

知识扩展 (选学)

MQTT 5.0 新特性

MQTT 5.0 相比 3.1.1 增加了许多功能:

// 消息过期时间
let properties = PublishProperties {
    message_expiry_interval: Some(60), // 60秒后过期
    ..Default::default()
};

// 用户属性(元数据)
let user_properties = vec![
    ("device-type".to_string(), "sensor".to_string()),
    ("version".to_string(), "1.0".to_string()),
];

TLS 加密连接

use rumqttc::{MqttOptions, Transport};

let mut mqttoptions = MqttOptions::new("secure-client", "broker.example.com", 8883);

// 配置 TLS
mqttoptions.set_transport(Transport::tls_with_config(
    rumqttc::TlsConfiguration::Native
));

小结

核心要点

  1. 消息队列 实现了生产者与消费者的解耦
  2. MQTT 是轻量级的发布/订阅协议,适合 IoT
  3. Topic 使用层级结构组织消息,+# 是通配符
  4. QoS 控制消息可靠性:0(最快) → 2(最可靠)
  5. 同步客户端 使用 Client异步客户端 使用 AsyncClient
  6. 遗嘱消息 在意外断开时自动通知其他客户端

关键术语

English中文说明
MQTT消息队列遥测传输轻量级消息协议
Broker消息代理消息中转服务器
Topic主题消息的地址标识
Publish发布发送消息到主题
Subscribe订阅注册接收主题消息
QoS服务质量消息可靠性等级
Payload消息载荷实际传输的数据
Retained保留消息存储在 Broker 的最后一条消息
LastWill遗嘱消息意外断开时自动发送的消息

下一步

  • 学习 服务框架 - 基于 MQTT 的微服务架构
  • 了解 gRPC - 另一种服务通信方式
  • 探索 Tokio - 异步运行时基础

术语表

English中文
Message Queue消息队列
MQTT消息队列遥测传输
Broker消息代理
Topic主题
Publish发布
Subscribe订阅
QoS服务质量
Payload载荷/消息体
Retained Message保留消息
Last Will遗嘱消息
Keep Alive保持连接
Clean Session清除会话
Persistent Session持久会话

完整示例:crates/awesome/src/mq/rumqtt_sample.rs


继续学习

  • 下一步:服务框架 - 生产级服务架构
  • 相关:Tokio 异步运行时 - rumqttc 的基础
  • 实战:尝试连接公共 MQTT Broker 如 test.mosquitto.org

💡 记住:MQTT 的核心是"发布/订阅解耦"——生产者无需知道消费者是谁,只需关注消息主题。这种松耦合架构让系统更具弹性和可扩展性!

MQTT 消息协议

开篇故事

想象你在设计一个智能家居系统。温度传感器需要每分钟上报数据,门锁需要接收开关命令,摄像头需要在有人移动时发送警报。如果每个设备都直接连接服务器,服务器会被大量连接压垮。MQTT 协议就像是一个智能邮局——设备只需把消息发送到邮局(Broker),邮局负责将消息分发给需要的订阅者。

MQTT(Message Queuing Telemetry Transport)是轻量级的发布/订阅消息协议,特别适合物联网场景——从智能家居到工业传感器,MQTT 都能稳定可靠地传递消息。


本章适合谁

如果你已经理解 Rust 异步编程基础,现在想学习:

  • MQTT 协议的发布/订阅模式
  • 如何使用 rumqttc 客户端库连接 MQTT Broker
  • 实现物联网设备间的消息通信

本章适合你。


你会学到什么

完成本章后,你可以:

  1. 理解 MQTT 的核心概念(Broker、Topic、QoS)
  2. 使用 rumqttc 创建同步和异步 MQTT 客户端
  3. 实现消息的发布(Publish)和订阅(Subscribe)
  4. 理解 QoS 等级及其对消息可靠性的影响
  5. 处理连接保持和重连逻辑
  6. 编写基于 MQTT 的物联网通信程序

前置要求

学习本章前,你需要理解:

  • 异步编程 - async/await 基础
  • Tokio - 使用 Tokio 异步运行时
  • 安装 MQTT Broker(推荐 Mosquitto 或 EMQX)用于测试

安装 MQTT Broker

# macOS
brew install mosquitto
brew services start mosquitto

# 或使用 Docker
docker run -d -p 1883:1883 eclipse-mosquitto

依赖安装

运行以下命令安装所需依赖:

cargo add tokio --features full
cargo add rumqttc
cargo add serde --features derive
cargo add serde_json

第一个例子

让我们看一个最简单的 MQTT 同步客户端示例:

use rumqttc::{Client, MqttOptions, QoS};
use std::time::Duration;

fn main() {
    // 配置 MQTT 连接选项
    let mut mqttoptions = MqttOptions::new("client-001", "127.0.0.1", 1883);
    mqttoptions.set_keep_alive(Duration::from_secs(5));

    // 创建客户端和连接
    let (mut client, mut connection) = Client::new(mqttoptions, 20);

    // 订阅主题
    client.subscribe("hello/rumqtt", QoS::AtMostOnce).unwrap();
    println!("已订阅主题 'hello/rumqtt'");

    // 在独立线程中发布消息
    std::thread::spawn(move || {
        for i in 0..10 {
            client.publish("hello/rumqtt", QoS::AtLeastOnce, false, vec![i; i as usize]).unwrap();
            println!("发送消息 {}", i);
            std::thread::sleep(Duration::from_millis(500));
        }
    });

    // 接收消息
    for notification in connection.iter() {
        println!("收到通知: {:?}", notification);
    }
}

发生了什么?

  1. MqttOptions 配置 MQTT Broker 地址和客户端 ID
  2. Client::new() 创建同步客户端,返回 (client, connection) 元组
  3. subscribe() 订阅指定主题,准备接收消息
  4. publish() 向主题发送消息,所有订阅者都会收到
  5. connection.iter() 阻塞等待并处理入站消息

完整示例:crates/awesome/src/mq/rumqtt_sample.rs


原理解析

MQTT 架构概览

┌─────────────────────────────────────────────────────────────────────┐
│                        MQTT 发布/订阅架构                             │
│                                                                      │
│   ┌──────────────┐         ┌──────────────┐         ┌──────────────┐│
│   │  Publisher   │         │              │         │  Subscriber  ││
│   │  (发布者)     │────────→│ MQTT Broker  │←────────│  (订阅者)     ││
│   │              │ publish │  (消息代理)   │ forward │              ││
│   └──────────────┘         └──────┬───────┘         └──────────────┘│
│                                   │                                  │
│   ┌──────────────┐         ┌──────┴───────┐         ┌──────────────┐│
│   │  Publisher   │         │              │         │  Subscriber  ││
│   │  (传感器)     │────────→│              │←────────│  (控制器)     ││
│   └──────────────┘         └──────────────┘         └──────────────┘│
│                                                                      │
│   主题层级:                                                            │
│   home/                                                               │
│   ├── bedroom/                                                        │
│   │   ├── temperature  ← 传感器发布到这里                               │
│   │   └── humidity                                                    │
│   └── livingroom/                                                     │
│       ├── light          ← 控制器订阅这里                               │
│       └── temperature                                                 │
│                                                                      │
│   通配符: home/+/temperature  匹配所有房间温度                          │
│          home/bedroom/#      匹配卧室所有子主题                         │
└─────────────────────────────────────────────────────────────────────┘

消息生命周期(QoS 1 流程)

Publisher                           Broker                          Subscriber
   │                                   │                               │
   │  1. CONNECT                       │                               │
   │ ────────────────────────────────→ │                               │
   │                                   │  2. CONNACK                   │
   │ ←──────────────────────────────── │                               │
   │                                   │                               │
   │  3. SUBSCRIBE (由订阅者发送)       │                               │
   │ ────────────────────────────────→ │                               │
   │                                   │  4. SUBACK                    │
   │ ←──────────────────────────────── │                               │
   │                                   │                               │
   │  5. PUBLISH (QoS 1)               │                               │
   │ ────────────────────────────────→ │                               │
   │                                   │  6. 匹配主题                   │
   │                                   │ ────────────────────────────→ │
   │                                   │                               │
   │                                   │  7. PUBLISH (转发)             │
   │                                   │ ←──────────────────────────── │
   │                                   │                               │
   │                                   │  8. PUBACK                    │
   │                                   │ ────────────────────────────→ │
   │  9. PUBACK                        │                               │
   │ ←──────────────────────────────── │                               │
   │                                   │                               │

核心概念

1. MQTT Broker(消息代理)

MQTT Broker 是消息的中转站,负责接收发布者的消息并转发给订阅者:

// 连接到本地 MQTT Broker
let mut mqttoptions = MqttOptions::new("client-id", "127.0.0.1", 1883);

// 或连接到公共测试 Broker
let mut mqttoptions = MqttOptions::new("client-id", "test.mosquitto.org", 1883);

mqttoptions.set_keep_alive(Duration::from_secs(5));

2. Topic(主题)

主题是消息的地址,使用层级结构:

// 简单主题
let topic = "sensors/temperature";

// 多层主题
let topic = "home/bedroom/temperature";

// 使用通配符订阅
client.subscribe("sensors/+/temperature", QoS::AtMostOnce).unwrap(); // + 匹配一级
client.subscribe("home/#", QoS::AtMostOnce).unwrap(); // # 匹配多级

3. QoS(服务质量等级)

QoS 等级名称说明应用场景
0AtMostOnce最多一次,不确认高频数据,丢失可接受
1AtLeastOnce至少一次,需确认关键数据,允许重复
2ExactlyOnce恰好一次,四次握手关键命令,不可重复
use rumqttc::QoS;

// QoS 0: 发送即忘
client.publish("sensor/data", QoS::AtMostOnce, false, payload).unwrap();

// QoS 1: 确保送达
client.publish("device/command", QoS::AtLeastOnce, false, payload).unwrap();

// QoS 2: 确保仅送达一次(开销最大)
client.publish("payment/confirm", QoS::ExactlyOnce, false, payload).unwrap();

异步客户端架构

┌──────────────────────────────────────────────────────────────────┐
│                        Tokio Runtime                              │
│                                                                   │
│  ┌──────────────┐        ┌──────────────┐                        │
│  │  Publisher   │        │  EventLoop   │                        │
│  │   Task       │        │   Task       │                        │
│  │              │        │              │                        │
│  │  client.     │───────→│ eventloop.   │←───────┐               │
│  │  publish()   │  send  │ poll().await │        │               │
│  │              │        │              │        │               │
│  └──────────────┘        └──────┬───────┘        │               │
│                                 │                │               │
│                                 │  TCP/TLS       │               │
│                                 ▼                │               │
│                        ┌──────────────┐          │               │
│                        │ MQTT Broker  │──────────┘               │
│                        │  127.0.0.1   │  publish msg             │
│                        └──────────────┘                          │
└──────────────────────────────────────────────────────────────────┘

异步客户端完整示例

use rumqttc::{AsyncClient, Event, MqttOptions, Packet, QoS};

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("async-client", "127.0.0.1", 1883);
    let (mut client, mut eventloop) = AsyncClient::new(mqttoptions, 15);

    // 异步订阅
    client.subscribe("hello/rumqtt", QoS::AtMostOnce).await.unwrap();

    // 异步发布
    tokio::spawn(async move {
        for i in 0..10 {
            client.publish("hello/rumqtt", QoS::AtLeastOnce, false, vec![i]).await.unwrap();
            tokio::time::sleep(Duration::from_secs(3)).await;
        }
    });

    // 异步处理事件
    loop {
        match eventloop.poll().await {
            Ok(Event::Incoming(Packet::Publish(p))) => {
                println!("收到消息: {:?}", p.payload);
            }
            Err(e) => {
                eprintln!("错误: {:?}", e);
                break;
            }
            _ => {}
        }
    }
}

MQTT 连接状态机

                  ┌─────────────┐
                  │  Disconnected│
                  └──────┬──────┘
                         │ CONNECT
                         ▼
                  ┌─────────────┐
            ┌─────│  Connecting  │─────┐
            │     └──────┬──────┘     │
            │            │ CONNACK    │
            │            ▼            │
            │     ┌─────────────┐     │
            │     │  Connected   │     │
            │     └──────┬──────┘     │
            │            │            │
            │     ┌──────┴──────┐     │
            │     │             │     │
            ▼     ▼             │     ▼
      ┌──────────┐      ┌──────────┐  ┌──────────┐
      │Publishing│      │Subscribing│  │Pinging   │
      └────┬─────┘      └────┬─────┘  └────┬─────┘
           │                 │              │
           └─────────────────┴──────────────┘
                         │
                         │ DISCONNECT / Error
                         ▼
                  ┌─────────────┐
                  │  Disconnected│
                  └─────────────┘

常见错误

错误 1: 忘记处理连接保持

// ❌ 错误:没有设置 keep_alive
let mqttoptions = MqttOptions::new("client", "broker", 1883);

// ✅ 正确:设置 keep_alive 保持连接
let mut mqttoptions = MqttOptions::new("client", "broker", 1883);
mqttoptions.set_keep_alive(Duration::from_secs(5));

问题:没有 keep_alive,Broker 会在空闲后断开连接。

错误 2: 同步客户端在异步上下文中使用

// ❌ 错误:在 async 函数中使用同步 Client
async fn bad_example() {
    let (client, _) = Client::new(mqttoptions, 10); // 阻塞!
    client.subscribe("topic", QoS::AtMostOnce).unwrap(); // 阻塞!
}

// ✅ 正确:使用 AsyncClient
async fn good_example() {
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    client.subscribe("topic", QoS::AtMostOnce).await.unwrap();
}

错误 3: 没有处理连接断开和重连

// ❌ 错误:连接断开时直接退出
loop {
    let notification = connection.iter().next().unwrap(); // 断开时 panic
}

// ✅ 正确:优雅处理连接错误
loop {
    match eventloop.poll().await {
        Ok(event) => handle_event(event),
        Err(e) => {
            eprintln!("连接错误: {:?}, 尝试重连...", e);
            tokio::time::sleep(Duration::from_secs(5)).await;
            // 重连逻辑
        }
    }
}

错误 4: 主题名称包含非法字符

// ❌ 错误:主题包含空格和特殊字符
let topic = "my topic with spaces";

// ✅ 正确:使用合法字符
let topic = "my/topic/with/hierarchy";

// 合法字符:字母、数字、下划线、正斜杠
// 正斜杠用于层级分隔

动手练习

练习 1: 修复订阅示例

下面的代码有什么问题?

fn main() {
    let mqttoptions = MqttOptions::new("client", "127.0.0.1", 1883);
    let (client, mut connection) = Client::new(mqttoptions, 10);
    
    // 订阅主题
    client.subscribe("test/topic", QoS::AtMostOnce).unwrap();
    
    // 发送消息
    client.publish("test/topic", QoS::AtLeastOnce, false, vec![1, 2, 3]).unwrap();
    
    // 接收消息
    while let Some(notification) = connection.iter().next() {
        println!("{:?}", notification);
    }
}
点击查看答案与解析

问题

  1. 没有设置 keep_alive,连接可能被 Broker 断开
  2. connection.iter() 返回 Result,需要正确处理
  3. 发送和接收在同一线程,没有并发处理

修复方案

use rumqttc::{Client, MqttOptions, QoS, Packet, Event};
use std::time::Duration;
use std::thread;

fn main() {
    let mut mqttoptions = MqttOptions::new("client", "127.0.0.1", 1883);
    mqttoptions.set_keep_alive(Duration::from_secs(5)); // ✅ 保持连接
    
    let (mut client, mut connection) = Client::new(mqttoptions, 10);
    
    // 订阅主题
    client.subscribe("test/topic", QoS::AtMostOnce).unwrap();
    
    // 在独立线程发送消息 ✅
    thread::spawn(move || {
        for i in 0..5 {
            client.publish("test/topic", QoS::AtLeastOnce, false, vec![i]).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });
    
    // 接收消息
    for notification in connection.iter() {
        match notification {
            Ok(Event::Incoming(Packet::Publish(p))) => {
                println!("收到消息: {:?}", p.payload);
            }
            Err(e) => eprintln!("错误: {:?}", e), // ✅ 错误处理
            _ => {}
        }
    }
}

练习 2: 实现温度监控器

补全下面的代码,实现一个简单的温度监控器:

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("temp-monitor", "127.0.0.1", 1883);
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    
    // TODO: 订阅温度主题 "home/+/temperature"
    // TODO: 当温度超过 30 度时打印警告
    
    loop {
        match eventloop.poll().await {
            // TODO: 处理收到的消息
            _ => {}
        }
    }
}
点击查看答案
use rumqttc::{AsyncClient, MqttOptions, QoS, Packet, Event};

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("temp-monitor", "127.0.0.1", 1883);
    mqttoptions.set_keep_alive(Duration::from_secs(5));
    
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    
    // 订阅所有房间的温度
    client.subscribe("home/+/temperature", QoS::AtMostOnce).await.unwrap();
    println!("已订阅温度主题");
    
    loop {
        match eventloop.poll().await {
            Ok(Event::Incoming(Packet::Publish(p))) => {
                // 解析温度值
                if let Ok(temp_str) = String::from_utf8(p.payload.to_vec()) {
                    if let Ok(temp) = temp_str.parse::<f32>() {
                        println!("[{}] 温度: {:.1}°C", p.topic, temp);
                        
                        // 超过 30 度报警
                        if temp > 30.0 {
                            println!("⚠️ 警告:{} 温度过高!", p.topic);
                        }
                    }
                }
            }
            Ok(Event::Incoming(Packet::ConnAck(_))) => {
                println!("已连接到 Broker");
            }
            Err(e) => {
                eprintln!("连接错误: {:?}", e);
                tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
            }
            _ => {}
        }
    }
}

练习 3: 理解 QoS 等级

预测以下代码中消息的传输可靠性(假设网络不稳定):

// 传感器 A:QoS 0
client.publish("sensor/a", QoS::AtMostOnce, false, vec![1]).await.unwrap();

// 传感器 B:QoS 1  
client.publish("sensor/b", QoS::AtLeastOnce, false, vec![2]).await.unwrap();

// 传感器 C:QoS 2
client.publish("sensor/c", QoS::ExactlyOnce, false, vec![3]).await.unwrap();

如果网络突然中断,哪些消息可能丢失?

点击查看解析

结果

传感器QoS可能丢失?原因
A0✅ 可能丢失发送即忘,无确认
B1❌ 不会丢失需要 ACK,会重试
C2❌ 不会丢失四次握手确保送达

关键点

  • QoS 0:性能最好,但不可靠
  • QoS 1:可靠性+性能平衡,可能重复
  • QoS 2:最高可靠性,但开销最大

适用场景

  • QoS 0:高频传感器数据(每秒上报)
  • QoS 1:设备控制命令(开关灯)
  • QoS 2:支付、关键配置更新

实际应用

应用场景 1: 智能家居控制中心

use rumqttc::{AsyncClient, MqttOptions, QoS, Packet, Event};
use std::collections::HashMap;
use tokio::sync::RwLock;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("home-controller", "127.0.0.1", 1883);
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    
    // 存储设备状态
    let devices: Arc<RwLock<HashMap<String, String>>> = Arc::new(RwLock::new(HashMap::new()));
    
    // 订阅所有设备主题
    client.subscribe("home/+/status", QoS::AtMostOnce).await.unwrap();
    client.subscribe("home/+/command", QoS::AtLeastOnce).await.unwrap();
    
    loop {
        match eventloop.poll().await {
            Ok(Event::Incoming(Packet::Publish(p))) => {
                let topic = p.topic;
                let payload = String::from_utf8_lossy(&p.payload);
                
                if topic.contains("/status") {
                    // 更新设备状态
                    let mut dev = devices.write().await;
                    dev.insert(topic.clone(), payload.to_string());
                    println!("设备 [{}] 状态: {}", topic, payload);
                }
            }
            Err(e) => {
                eprintln!("MQTT 错误: {:?}", e);
            }
            _ => {}
        }
    }
}

应用场景 2: 传感器数据采集

use rumqttc::{AsyncClient, MqttOptions, QoS};
use tokio::time::{interval, Duration};
use rand::Rng;

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("sensor-sim", "127.0.0.1", 1883);
    let (client, _) = AsyncClient::new(mqttoptions, 10);
    
    let mut ticker = interval(Duration::from_secs(5));
    let mut rng = rand::thread_rng();
    
    loop {
        ticker.tick().await;
        
        // 模拟传感器数据
        let temperature = 20.0 + rng.gen::<f32>() * 10.0;
        let humidity = 40.0 + rng.gen::<f32>() * 30.0;
        
        // 发布温度(QoS 0,高频数据)
        client.publish(
            "factory/sensor1/temperature",
            QoS::AtMostOnce,
            false,
            format!("{:.2}", temperature).into_bytes()
        ).await.unwrap();
        
        // 发布湿度(QoS 0)
        client.publish(
            "factory/sensor1/humidity",
            QoS::AtMostOnce,
            false,
            format!("{:.2}", humidity).into_bytes()
        ).await.unwrap();
        
        println!("已发布: T={:.1}°C, H={:.1}%", temperature, humidity);
    }
}

应用场景 3: 带遗嘱消息的客户端

use rumqttc::{AsyncClient, LastWill, MqttOptions, QoS};

#[tokio::main]
async fn main() {
    let mut mqttoptions = MqttOptions::new("device-001", "127.0.0.1", 1883);
    
    // 设置遗嘱消息:意外断开时自动发布
    let will = LastWill::new(
        "device/001/status",
        b"offline",
        QoS::AtLeastOnce,
        true // retained 消息
    );
    mqttoptions.set_last_will(will);
    
    let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
    
    // 上线时发送状态
    client.publish(
        "device/001/status",
        QoS::AtLeastOnce,
        true, // retained
        b"online"
    ).await.unwrap();
    
    println!("设备已上线");
    
    // 保持连接
    loop {
        match eventloop.poll().await {
            Err(e) => {
                eprintln!("连接断开: {:?}", e);
                break;
            }
            _ => {}
        }
    }
}

故障排查 (FAQ)

Q: 连接失败 "Connection refused" 怎么办?

A: 检查以下几点:

  1. Broker 是否运行

    # 测试 Broker 连通性
    mosquitto_pub -t test -m "hello" -h 127.0.0.1
    
  2. 端口是否正确

    • MQTT 默认端口:1883
    • MQTT over TLS:8883
    • WebSocket:8083
  3. 防火墙设置

    # 开放端口(Linux)
    sudo ufw allow 1883
    

Q: 什么时候用同步客户端,什么时候用异步客户端?

A:

场景推荐客户端原因
简单脚本、命令行工具Client简单直接
需要与 Tokio 集成AsyncClient兼容性
高性能服务器AsyncClient非阻塞,高吞吐
嵌入式/资源受限Client更少的依赖

Q: 如何处理消息积压?

A: 使用背压控制和合理配置:

// 增大接收缓冲区
let (client, eventloop) = AsyncClient::new(mqttoptions, 100); // 缓冲区 100

// 使用多消费者处理
for _ in 0..4 {
    let rx = rx.clone();
    tokio::spawn(async move {
        while let Some(msg) = rx.recv().await {
            process_message(msg).await;
        }
    });
}

Q: MQTT vs Kafka vs RabbitMQ 如何选择?

A:

特性MQTTKafkaRabbitMQ
协议复杂度简单中等复杂
适用场景IoT、移动端大数据流企业消息
消息持久化可选强制可选
QoS 支持内置需配置需配置
资源占用极低中等
典型部署边缘设备数据中心企业服务

知识扩展 (选学)

MQTT 5.0 新特性

MQTT 5.0 相比 3.1.1 增加了许多功能:

// 消息过期时间
let properties = PublishProperties {
    message_expiry_interval: Some(60), // 60秒后过期
    ..Default::default()
};

// 用户属性(元数据)
let user_properties = vec![
    ("device-type".to_string(), "sensor".to_string()),
    ("version".to_string(), "1.0".to_string()),
];

TLS 加密连接

use rumqttc::{MqttOptions, Transport};

let mut mqttoptions = MqttOptions::new("secure-client", "broker.example.com", 8883);

// 配置 TLS
mqttoptions.set_transport(Transport::tls_with_config(
    rumqttc::TlsConfiguration::Native
));

小结

核心要点

  1. MQTT 是轻量级的发布/订阅协议,适合 IoT
  2. Topic 使用层级结构组织消息,+# 是通配符
  3. QoS 控制消息可靠性:0(最快) → 2(最可靠)
  4. 同步客户端 使用 Client异步客户端 使用 AsyncClient
  5. 遗嘱消息 在意外断开时自动通知其他客户端

关键术语

English中文说明
MQTT消息队列遥测传输轻量级消息协议
Broker消息代理消息中转服务器
Topic主题消息的地址标识
Publish发布发送消息到主题
Subscribe订阅注册接收主题消息
QoS服务质量消息可靠性等级
Payload消息载荷实际传输的数据
Retained保留消息存储在 Broker 的最后一条消息
LastWill遗嘱消息意外断开时自动发送的消息

下一步

  • 学习 消息队列总览 - 了解 Rust 生态各种 MQ 方案
  • 了解 服务框架 - 基于 MQTT 的微服务架构
  • 探索 Tokio - 异步运行时基础

术语表

English中文
MQTT消息队列遥测传输
Broker消息代理
Topic主题
Publish发布
Subscribe订阅
QoS服务质量
Payload载荷/消息体
Retained Message保留消息
Last Will遗嘱消息
Keep Alive保持连接
Clean Session清除会话
Persistent Session持久会话

完整示例:crates/awesome/src/mq/rumqtt_sample.rs


继续学习

  • 上一步:消息队列总览 - Rust 生态各种 MQ 方案对比
  • 下一步:服务框架 - 生产级服务架构
  • 相关:Tokio 异步运行时 - rumqttc 的基础
  • 实战:尝试连接公共 MQTT Broker 如 test.mosquitto.org

💡 记住:MQTT 的核心是"发布/订阅解耦"——生产者无需知道消费者是谁,只需关注消息主题。这种松耦合架构让系统更具弹性和可扩展性!

模板引擎

开篇故事

想象你需要发送 1000 封个性化的欢迎邮件,每封邮件包含用户名、注册日期、专属优惠码。如果手动替换每个变量,不仅耗时而且容易出错。模板引擎就像一个智能的邮件合并工具——你定义模板,它自动填充变量,生成最终内容。

在 Rust 生态中,有三种主流的模板方案:Tera(Web 渲染)、Liquid(安全模板)、Pest(自定义语法解析)。每种方案解决不同的问题。


本章适合谁

如果你需要生成 HTML 页面、配置文件、邮件内容、报告文档、或自定义 DSL,本章适合你。


你会学到什么

完成本章后,你可以:

  1. 使用 Tera 模板引擎渲染 HTML(循环、条件、过滤器)
  2. 使用 Liquid 模板生成安全的内容(内联 + 文件)
  3. 使用 Pest 解析器构建自定义语法(PEG 语法)
  4. 根据场景选择合适的模板引擎

前置要求

  • Rust 基础语法
  • 字符串处理
  • Serde 序列化(用于 Tera 结构体渲染)

依赖安装

运行以下命令安装所需依赖:

cargo add tera
cargo add liquid
cargo add pest
cargo add pest_derive
cargo add serde --features derive
cargo add chrono

第一个例子

最简单的 Tera 模板渲染:

use tera::{Context, Tera};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tera = Tera::new("templates/**/*.html")?;
    let mut context = Context::new();
    context.insert("name", &"Alice");
    
    let rendered = tera.render("hello.html", &context)?;
    println!("{}", rendered);
    Ok(())
}

模板文件 templates/hello.html:

<h1>Hello, {{ name }}!</h1>

完整示例:crates/awesome/src/templates/tera_sample.rs


原理解析

模板引擎对比

引擎语法适用场景安全性
TeraJinja2-likeHTML 页面、Web 应用中等
LiquidShopify Liquid静态网站、用户自定义模板高(沙箱)
PestPEG 解析器自定义 DSL、语法解析N/A(解析器)

Tera 完整示例

Tera 是最常用的 Rust 模板引擎,语法类似 Jinja2/Python。

完整代码示例(来自 tera_sample.rs):

use tera::{Context, Tera};
use serde::Serialize;

#[derive(Serialize)]
struct User {
    id: u32,
    name: String,
}

fn tera_sample() -> Result<(), tera::Error> {
    // 1. 创建 Tera 实例并加载模板
    let tera = Tera::new("templates/**/*")?;

    // 2. 创建 Context 对象
    let mut context = Context::new();
    context.insert("title", &"My Webpage");
    context.insert("greeting", &"Hello");
    context.insert("name", &"World");
    context.insert("show_message", &true);
    context.insert("message", &"This is a test message.");
    context.insert("items", &vec!["apple", "banana", "orange"]);
    context.insert("now", &chrono::Utc::now().naive_utc());

    // 插入结构体(自动序列化)
    let user = User { id: 123, name: "Alice".to_string() };
    context.insert("user", &user);

    // 3. 渲染模板
    let rendered = tera.render("tera/index.html", &context)?;
    println!("{}", rendered);

    Ok(())
}

模板文件 tera/index.html

<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
    <h1>{{ greeting }}, {{ name }}!</h1>
    
    {# 条件判断 #}
    {% if show_message %}
        <p>{{ message }}</p>
    {% endif %}
    
    {# 循环迭代 #}
    <ul>
    {% for item in items %}
        <li>{{ item | upper }}</li>
    {% endfor %}
    </ul>
    
    {# 结构体访问 #}
    <p>用户:{{ user.name }} (ID: {{ user.id }})</p>
    
    {# 日期格式化 #}
    <p>时间:{{ now | date(format="%Y-%m-%d %H:%M") }}</p>
</body>
</html>

Tera 核心功能

  • 变量替换:{{ variable }}
  • 条件判断:{% if condition %} ... {% endif %}
  • 循环迭代:{% for item in list %} ... {% endfor %}
  • 过滤器:{{ name | upper }}{{ date | date(format="%Y") }}
  • 结构体访问:{{ user.name }}

Liquid 完整示例

Liquid 是 Shopify 开发的安全模板引擎,适合用户自定义模板场景。

内联模板示例(来自 liquid_sample.rs):

use liquid::{ParserBuilder, ValueView};

fn liquid_sample() -> Result<(), liquid::Error> {
    // 1. 定义模板(内联字符串)
    let template = r#"
    <ul>
        {% for item in items %}
            <li>{{ item }}</li>
        {% endfor %}
    </ul>
    "#;

    // 2. 创建解析器
    let parser = ParserBuilder::with_stdlib().build()?;
    let template = parser.parse(template)?;

    // 3. 创建上下文
    let mut context = liquid::object!({
        "items": vec!["apple", "banana", "orange"],
    });

    // 4. 渲染
    let output = template.render(&mut context)?;
    println!("{}", output);
    Ok(())
}

文件模板示例

fn liquid_file_sample() {
    // 1. 从文件加载模板
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse_file("templates/liquid/data.tpl")
        .unwrap();

    // 2. 准备数据
    let user = User { id: 123, name: "Alice".to_string() };
    let mut context = liquid::object!({
        "items": vec!["apple", "banana", "orange"],
        "user": &user,
    });

    // 3. 渲染
    let output = template.render(&mut context).unwrap();
    println!("{}", output);
}

模板文件 templates/liquid/data.tpl

<h1>Liquid Template</h1>
<p>用户:{{ user.name }} (ID: {{ user.id }})</p>
<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

Liquid 优势

  • 安全性强(沙箱环境,无法执行任意代码)
  • Shopify 生态标准
  • 适合用户自定义模板(如 CMS)

Pest 完整示例

Pest 不是传统模板引擎,而是 PEG(Parsing Expression Grammar)解析器生成器。它用于构建自定义语法解析器。

完整代码示例(来自 pest_sample.rs):

use pest::Parser;
use pest_derive::Parser;

// 1. 定义解析器(使用 derive 宏加载语法文件)
#[derive(Parser)]
#[grammar = "templates/pest/grammar.pest"]
pub struct ExpressionParser;

fn pest_sample() {
    let input = "1 + 2 * 3";
    
    // 2. 解析输入
    let pairs = ExpressionParser::parse(Rule::expression, input).unwrap();

    // 3. 遍历解析结果
    for pair in pairs {
        println!("Rule: {:?}", pair.as_rule());
        println!("Span: {:?}", pair.as_span());
        println!("Text: {}", pair.as_str());
    }
}

语法文件 templates/pest/grammar.pest

// 定义表达式语法规则
expression = _{ term ~ (("+" | "-") ~ term)* }
term = _{ factor ~ (("*" | "/") ~ factor)* }
factor = _{ number | "(" ~ expression ~ ")" }
number = @{ ASCII_DIGIT+ }

WHITESPACE = _{ " " | "\t" }

Pest 适用场景

  • 自定义 DSL(领域特定语言)
  • 配置文件解析
  • 查询语言解析
  • 数学表达式解析

常见错误

错误 1: 模板文件路径错误

确保模板文件路径正确,使用相对于 Cargo.toml 的路径:

// ❌ 错误:绝对路径
let tera = Tera::new("/absolute/path/templates/**/*.html")?;

// ✅ 正确:相对路径
let tera = Tera::new("templates/**/*.html")?;

错误 2: 变量未定义

使用 context.insert() 添加所有模板需要的变量:

// ❌ 错误:模板使用 {{ name }} 但未插入
let rendered = tera.render("hello.html", &context)?;

// ✅ 正确:先插入变量
context.insert("name", &"Alice");
let rendered = tera.render("hello.html", &context)?;

错误 3: Pest 语法文件路径

Pest 的 #[grammar] 路径相对于 src/ 目录:

// ❌ 错误:路径不正确
#[grammar = "grammar.pest"]

// ✅ 正确:相对于 src/
#[grammar = "templates/pest/grammar.pest"]

知识检查

问题 1: 哪个模板引擎适合 HTML 页面渲染?

问题 2: Liquid 模板引擎的主要优势是什么?

问题 3: Pest 适合什么场景?

点击查看答案与解析
  1. Tera(Jinja2 语法,Web 友好,支持结构体序列化)
  2. 安全性强,沙箱环境,适合用户自定义模板(如 CMS)
  3. 自定义 DSL、语法解析、配置文件解析(PEG 解析器)

关键理解: 选择模板引擎取决于你的需求:Web 用 Tera,安全用 Liquid,解析用 Pest。


延伸阅读

学习完模板引擎后,你可能还想了解:

  • Askama - 编译时模板(零运行时开销)
  • Handlebars - JavaScript Handlebars 的 Rust 实现
  • Minijinja - 轻量级 Jinja2 实现

选择建议:

  • Web 应用 → Tera
  • 用户自定义模板 → Liquid
  • 自定义语法 → Pest
  • 极致性能 → Askama(编译时渲染)

小结

核心要点:Tera 用于 HTML、Liquid 用于安全模板、Pest 用于自定义语法

完整示例:

Algo Sample

LeetCode Solution Sample

hello-rust

A Rust-based project demonstrating high-performance system programming with memory safety.

  • Language: Rust
  • Features: CLI tool, Async I/O
  • Repo: GitHub

hello-go

A Go project showcasing simple, concurrent web server implementation.

  • Language: Go
  • Features: REST API, Goroutines
  • Repo: GitHub

hello-python

A Python project for data analysis and visualization.

  • Language: Python
  • Features: Pandas, Matplotlib
  • Repo: GitHub

hello-js

A JavaScript project for interactive web applications.

  • Language: JavaScript
  • Features: React, Node.js
  • Repo: GitHub

常用操作速查

本章节提供 Rust 开发中最常用操作的代码片段,方便快速查找和复制使用。每个类别包含 3-5 个实用示例。


文件操作

读取文件

use std::fs;
use std::io::{self, BufRead};

// 一次性读取整个文件
fn read_entire_file() -> io::Result<String> {
    let content = fs::read_to_string("data.txt")?;
    Ok(content)
}

// 按行读取文件
fn read_lines(filename: &str) -> io::Result<Vec<String>> {
    let file = fs::File::open(filename)?;
    let reader = io::BufReader::new(file);
    let lines: Vec<String> = reader.lines().collect();
    Ok(lines)
}

// 读取二进制文件
fn read_binary_file() -> io::Result<Vec<u8>> {
    let data = fs::read("image.png")?;
    Ok(data)
}

写入文件

use std::fs::{self, File};
use std::io::Write;

// 写入字符串到文件
fn write_to_file(content: &str) -> io::Result<()> {
    fs::write("output.txt", content)?;
    Ok(())
}

// 追加内容到文件
fn append_to_file(content: &str) -> io::Result<()> {
    let mut file = fs::OpenOptions::new()
        .write(true)
        .append(true)
        .open("output.txt")?;
    
    writeln!(file, "{}", content)?;
    Ok(())
}

// 写入二进制数据
fn write_binary_data(data: &[u8]) -> io::Result<()> {
    fs::write("binary.dat", data)?;
    Ok(())
}

文件与目录操作

use std::fs;
use std::path::Path;

// 创建目录
fn create_directory() -> io::Result<()> {
    fs::create_dir_all("path/to/directory")?;
    Ok(())
}

// 复制文件
fn copy_file() -> io::Result<()> {
    fs::copy("source.txt", "destination.txt")?;
    Ok(())
}

// 删除文件或目录
fn delete_file_or_dir() -> io::Result<()> {
    fs::remove_file("file.txt")?;
    fs::remove_dir_all("directory")?;
    Ok(())
}

// 检查路径是否存在
fn check_path_exists(path: &str) -> bool {
    Path::new(path).exists()
}

集合操作

Vec 向量操作

// 创建和初始化
let mut vec: Vec<i32> = Vec::new();
let vec = vec![1, 2, 3, 4, 5];
let vec: Vec<i32> = (0..10).collect();

// 常用操作
vec.push(6);              // 添加元素
vec.pop();                // 移除末尾元素
vec.insert(0, 0);         // 在索引 0 插入
vec.remove(0);            // 移除索引 0 的元素
vec.len();                // 获取长度
vec.is_empty();           // 判断是否为空
vec.clear();              // 清空

// 查找和过滤
let first = vec.first();          // 获取第一个元素
let last = vec.last();            // 获取最后一个元素
let found = vec.iter().find(|&&x| x > 3);  // 查找元素
let filtered: Vec<&i32> = vec.iter().filter(|&&x| x > 2).collect();  // 过滤

// 排序和反转
vec.sort();               // 排序
vec.sort_by(|a, b| b.cmp(a));  // 自定义排序
vec.reverse();            // 反转
vec.dedup();             // 去重

HashMap 哈希表操作

use std::collections::HashMap;

// 创建和初始化
let mut map: HashMap<&str, i32> = HashMap::new();
let map = HashMap::from([("apple", 5), ("banana", 3)]);

// 基本操作
map.insert("orange", 10);          // 插入
map.remove("apple");                // 移除
map.get("banana");                  // 获取值 (Option<&i32>)
map.len();                          // 获取长度
map.is_empty();                     // 判断是否为空
map.clear();                        // 清空

// 安全访问
let value = map.get("key").unwrap_or(&0);  // 带默认值
let value = map.entry("key").or_insert(0); // 不存在则插入
*map.entry("count").or_insert(0) += 1;     // 计数器

// 遍历
for (key, value) in &map {
    println!("{}: {}", key, value);
}

// 只遍历键或值
for key in map.keys() { /* ... */ }
for value in map.values() { /* ... */ }

HashSet 哈希集合操作

use std::collections::HashSet;

// 创建和初始化
let mut set: HashSet<i32> = HashSet::new();
let set: HashSet<i32> = [1, 2, 3].iter().cloned().collect();

// 基本操作
set.insert(4);              // 插入元素
set.remove(&1);             // 移除元素
set.contains(&2);            // 检查是否存在
set.len();                   // 获取长度
set.is_empty();              // 判断是否为空

// 集合运算
let a: HashSet<i32> = [1, 2, 3].iter().cloned().collect();
let b: HashSet<i32> = [3, 4, 5].iter().cloned().collect();

let union: HashSet<&i32> = a.union(&b).collect();        // 并集 {1, 2, 3, 4, 5}
let intersection: HashSet<&i32> = a.intersection(&b).collect();  // 交集 {3}
let difference: HashSet<&i32> = a.difference(&b).collect();      // 差集 {1, 2}

字符串操作

创建与转换

// 创建字符串
let s1 = String::new();
let s2 = String::from("hello");
let s3 = "world".to_string();
let s4 = format!("{} {}", s2, s3);  // 格式化创建

// 从其他类型转换
let num_str = 42.to_string();
let bool_str = true.to_string();
let parsed: i32 = "42".parse().unwrap();  // 字符串转数字

// String 和 &str 转换
let string: String = "hello".to_string();
let slice: &str = &string;
let owned: String = slice.to_owned();

字符串修改

let mut s = String::from("hello");

// 追加内容
s.push_str(" world");    // 追加字符串
s.push('!');             // 追加字符

// 插入内容
s.insert(0, 'H');        // 在索引 0 插入字符
s.insert_str(6, "Rust ");  // 在索引 6 插入字符串

// 替换内容
s = s.replace("hello", "hi");
s = s.replacen("l", "L", 1);  // 只替换第一个

// 删除内容
s.remove(0);             // 删除指定位置的字符
s.truncate(5);           // 截断到指定长度
s.clear();               // 清空字符串

// 弹出字符
let last_char = s.pop();  // 弹出最后一个字符

字符串查询

let s = "Hello, Rust!";

// 长度和判空
let len = s.len();               // 长度
let is_empty = s.is_empty();     // 是否为空

// 查找
s.contains("Rust");              // 是否包含
s.starts_with("Hello");          // 是否以...开头
s.ends_with("!");                // 是否以...结尾
s.find("Rust");                  // 查找位置 (Option<usize>)
s.rfind("l");                    // 从右边查找

// 分割
let parts: Vec<&str> = "a,b,c".split(',').collect();
let lines: Vec<&str> = "line1\nline2".lines().collect();
let words: Vec<&str> = "hello world".split_whitespace().collect();

// 截取
let slice = &s[0..5];            // 切片 [0, 5)
let first_n = s.chars().take(5).collect::<String>();  // 前5个字符

错误处理

Result 处理

use std::fs::File;
use std::io::{self, Read};

// 基本错误处理
fn read_file_basic() -> Result<String, io::Error> {
    let mut file = File::open("data.txt")?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}

// 使用 match 处理
fn handle_with_match() {
    let result = File::open("data.txt");
    match result {
        Ok(file) => println!("文件打开成功"),
        Err(error) => println!("错误: {}", error),
    }
}

// unwrap 和 expect
let file = File::open("data.txt").unwrap();  // 失败时 panic
let file = File::open("data.txt").expect("无法打开文件");  // 带 panic 信息

// 提供默认值
let content = read_file_basic().unwrap_or_default();
let content = read_file_basic().unwrap_or_else(|err| {
    println!("读取失败: {}", err);
    String::new()
});

Option 处理

// 基本用法
let some_value: Option<i32> = Some(42);
let none_value: Option<i32> = None;

// 检查和提取
some_value.is_some();            // 是否有值
some_value.is_none();            // 是否无值
let value = some_value.unwrap(); // 提取值 (None 会 panic)

// 安全提取
let value = some_value.unwrap_or(0);        // 提供默认值
let value = some_value.unwrap_or_else(|| 0); // 闭包提供默认值
let value = some_value.unwrap_or_default(); // 类型默认值

// map 和 and_then
let doubled = some_value.map(|x| x * 2);          // Some(84)
let result = some_value.and_then(|x| Some(x + 1)); // Some(43)

// 使用 match
match some_value {
    Some(value) => println!("值为: {}", value),
    None => println!("无值"),
}

// if let 语法糖
if let Some(value) = some_value {
    println!("值为: {}", value);
}

自定义错误类型

use std::fmt;
use std::error::Error;

#[derive(Debug)]
enum MyError {
    IoError(std::io::Error),
    ParseError(std::num::ParseIntError),
    CustomError(String),
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyError::IoError(e) => write!(f, "IO错误: {}", e),
            MyError::ParseError(e) => write!(f, "解析错误: {}", e),
            MyError::CustomError(msg) => write!(f, "自定义错误: {}", msg),
        }
    }
}

impl Error for MyError {}

// 使用 thiserror 简化 (需要添加依赖)
// use thiserror::Error;
// 
// #[derive(Error, Debug)]
// enum MyError {
//     #[error("IO错误: {0}")]
//     Io(#[from] std::io::Error),
//     
//     #[error("解析错误: {0}")]
//     Parse(#[from] std::num::ParseIntError),
// }

异步基础

async/await 基本用法

use tokio::time::{sleep, Duration};

// 定义异步函数
async fn say_hello() {
    println!("Hello");
    sleep(Duration::from_secs(1)).await;
    println!("World");
}

// 异步函数返回值
async fn fetch_data() -> Result<String, reqwest::Error> {
    let response = reqwest::get("https://api.example.com/data").await?;
    let body = response.text().await?;
    Ok(body)
}

// 运行异步函数
#[tokio::main]
async fn main() {
    say_hello().await;
    match fetch_data().await {
        Ok(data) => println!("数据: {}", data),
        Err(e) => println!("错误: {}", e),
    }
}

并发执行

use tokio;

// 顺序执行
async fn sequential() {
    let r1 = async_function1().await;
    let r2 = async_function2().await;
}

// 并发执行
async fn concurrent() {
    let (r1, r2) = tokio::join!(
        async_function1(),
        async_function2(),
    );
}

// 使用 tokio::spawn 创建任务
async fn spawn_tasks() {
    let handle1 = tokio::spawn(async {
        // 任务 1
        println!("Task 1");
    });
    
    let handle2 = tokio::spawn(async {
        // 任务 2
        println!("Task 2");
    });
    
    let _ = tokio::join!(handle1, handle2);
}

// 使用 join_all 等待多个任务
async fn join_all_tasks() {
    let tasks: Vec<_> = (0..5).map(|i| {
        tokio::spawn(async move {
            println!("Task {}", i);
        })
    }).collect();
    
    let results = futures::future::join_all(tasks).await;
}

异步通道

use tokio::sync::mpsc;

// 多生产者单消费者通道
async fn mpsc_example() {
    let (tx, mut rx) = mpsc::channel(32);
    
    // 生产者
    let producer = tokio::spawn(async move {
        for i in 0..10 {
            tx.send(i).await.unwrap();
        }
    });
    
    // 消费者
    let consumer = tokio::spawn(async move {
        while let Some(value) = rx.recv().await {
            println!("收到: {}", value);
        }
    });
    
    let _ = tokio::join!(producer, consumer);
}

// oneshot 通道 (单次发送)
async fn oneshot_example() {
    let (tx, rx) = tokio::sync::oneshot::channel();
    
    tokio::spawn(async move {
        tx.send("Hello").unwrap();
    });
    
    if let Ok(msg) = rx.await {
        println!("收到: {}", msg);
    }
}

网络编程

TCP 客户端

use std::io::{self, Write};
use std::net::TcpStream;

// TCP 客户端
fn tcp_client() -> io::Result<()> {
    let mut stream = TcpStream::connect("127.0.0.1:8080")?;
    
    // 发送数据
    stream.write_all(b"Hello, Server!")?;
    
    // 读取响应
    let mut buffer = [0; 1024];
    let n = stream.read(&mut buffer)?;
    println!("收到响应: {}", String::from_utf8_lossy(&buffer[..n]));
    
    Ok(())
}

// 异步 TCP 客户端 (使用 tokio)
async fn async_tcp_client() -> io::Result<()> {
    use tokio::net::TcpStream;
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    
    let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
    stream.write_all(b"Hello, Server!").await?;
    
    let mut buffer = [0; 1024];
    let n = stream.read(&mut buffer).await?;
    println!("收到响应: {}", String::from_utf8_lossy(&buffer[..n]));
    
    Ok(())
}

TCP 服务器

use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

// TCP 服务器
fn tcp_server() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080")?;
    println!("服务器启动,监听 127.0.0.1:8080");
    
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                thread::spawn(|| {
                    handle_client(stream);
                });
            }
            Err(e) => println!("连接失败: {}", e),
        }
    }
    
    Ok(())
}

fn handle_client(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    while let Ok(n) = stream.read(&mut buffer) {
        if n == 0 { break; }
        
        // 回显
        stream.write_all(&buffer[..n]).unwrap();
    }
}

HTTP 客户端

// 使用 reqwest (需要添加依赖)
use reqwest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // GET 请求
    let response = reqwest::get("https://httpbin.org/get").await?;
    let body = response.text().await?;
    println!("GET 响应: {}", body);
    
    // POST 请求
    let client = reqwest::Client::new();
    let response = client
        .post("https://httpbin.org/post")
        .json(&serde_json::json!({
            "name": "Rust",
            "version": "1.70"
        }))
        .send()
        .await?;
    
    println!("POST 响应: {}", response.text().await?);
    
    Ok(())
}

序列化

JSON 序列化/反序列化

use serde::{Deserialize, Serialize};

// 定义结构体
#[derive(Serialize, Deserialize, Debug)]
struct User {
    id: u64,
    name: String,
    email: String,
    active: bool,
}

// 序列化为 JSON 字符串
fn serialize_json() {
    let user = User {
        id: 1,
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
        active: true,
    };
    
    let json = serde_json::to_string(&user).unwrap();
    println!("JSON: {}", json);
    // {"id":1,"name":"Alice","email":"alice@example.com","active":true}
    
    // 美化输出
    let pretty = serde_json::to_string_pretty(&user).unwrap();
    println!("Pretty JSON:\n{}", pretty);
}

// 从 JSON 反序列化
fn deserialize_json() {
    let json = r#"{
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
        "active": true
    }"#;
    
    let user: User = serde_json::from_str(json).unwrap();
    println!("User: {:?}", user);
}

// 处理泛型 JSON
fn parse_generic_json() {
    let json = r#"{"name": "Alice", "age": 30}"#;
    let value: serde_json::Value = serde_json::from_str(json).unwrap();
    
    let name = value["name"].as_str().unwrap();
    let age = value["age"].as_i64().unwrap();
    println!("Name: {}, Age: {}", name, age);
}

其他序列化格式

// YAML 序列化 (需要添加 serde_yaml 依赖)
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Config {
    database_url: String,
    port: u16,
}

fn yaml_example() {
    let config = Config {
        database_url: "localhost:5432".to_string(),
        port: 8080,
    };
    
    // 序列化为 YAML
    let yaml = serde_yaml::to_string(&config).unwrap();
    println!("YAML:\n{}", yaml);
    
    // 从 YAML 反序列化
    let config: Config = serde_yaml::from_str(&yaml).unwrap();
    println!("Config: {:?}", config);
}

// TOML 序列化 (需要添加 toml 依赖)
fn toml_example() {
    let config = Config {
        database_url: "localhost:5432".to_string(),
        port: 8080,
    };
    
    // 序列化为 TOML
    let toml = toml::to_string(&config).unwrap();
    println!("TOML:\n{}", toml);
    
    // 从 TOML 反序列化
    let config: Config = toml::from_str(&toml).unwrap();
    println!("Config: {:?}", config);
}

// 二进制序列化 (需要添加 bincode 依赖)
fn binary_example() {
    let config = Config {
        database_url: "localhost:5432".to_string(),
        port: 8080,
    };
    
    // 序列化为二进制
    let bytes = bincode::serialize(&config).unwrap();
    println!("Binary length: {} bytes", bytes.len());
    
    // 从二进制反序列化
    let config: Config = bincode::deserialize(&bytes).unwrap();
    println!("Config: {:?}", config);
}

测试

单元测试

// 在同一文件中编写测试
#[cfg(test)]
mod tests {
    // 需要导入父模块的内容
    use super::*;
    
    #[test]
    fn test_addition() {
        assert_eq!(2 + 2, 4);
    }
    
    #[test]
    fn test_string_concatenation() {
        let s1 = "Hello";
        let s2 = "World";
        assert_eq!(format!("{}, {}", s1, s2), "Hello, World");
    }
    
    #[test]
    fn test_with_assertions() {
        let value = Some(42);
        
        // 各种断言
        assert!(value.is_some());           // 布尔断言
        assert_eq!(value.unwrap(), 42);      // 相等断言
        assert_ne!(value, None);            // 不等断言
    }
    
    #[test]
    #[should_panic(expected = "division by zero")]
    fn test_panic() {
        panic!("division by zero");
    }
    
    #[test]
    fn test_result() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("Math is broken"))
        }
    }
}

集成测试

// tests/integration_test.rs

// 导入外部 crate 的公共 API
use my_crate::function_to_test;

#[test]
fn test_external_function() {
    assert_eq!(function_to_test(2), 4);
}

// 测试异步函数
#[tokio::test]
async fn test_async_function() {
    let result = async_function().await;
    assert!(result.is_ok());
}

// 测试错误情况
#[test]
fn test_error_case() {
    let result = fallible_function(-1);
    assert!(result.is_err());
}

测试辅助工具

// 测试前初始化
#[cfg(test)]
mod tests {
    use super::*;
    
    // 每个测试前运行
    fn setup() -> TestContext {
        TestContext::new()
    }
    
    #[test]
    fn test_with_setup() {
        let ctx = setup();
        // 使用 ctx 进行测试
    }
}

// 使用测试属性宏
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    #[ignore]  // 默认跳过此测试
    fn expensive_test() {
        // 运行: cargo test -- --ignored
    }
    
    // 测试特定功能
    #[test]
    #[cfg(feature = "advanced")]
    fn test_advanced_feature() {
        // 仅在 feature 启用时运行
    }
}

// 基准测试 (需要 nightly)
// #[bench]
// fn bench_addition(b: &mut test::Bencher) {
//     b.iter(|| 2 + 2);
// }

命令行参数

std::env::args

use std::env;

fn main() {
    // 获取所有参数
    let args: Vec<String> = env::args().collect();
    
    // args[0] 是程序名
    println!("程序名: {}", args[0]);
    
    // 遍历参数
    for (i, arg) in args.iter().enumerate() {
        println!("参数 {}: {}", i, arg);
    }
    
    // 简单参数解析
    if args.len() > 1 {
        let command = &args[1];
        match command.as_str() {
            "help" => println!("显示帮助"),
            "version" => println!("版本 1.0.0"),
            _ => println!("未知命令"),
        }
    }
}

使用 clap (推荐)

use clap::{Arg, Command};

fn main() {
    let matches = Command::new("MyApp")
        .version("1.0")
        .about("我的应用")
        .arg(Arg::new("input")
            .short('i')
            .long("input")
            .value_name("FILE")
            .help("输入文件")
            .required(true))
        .arg(Arg::new("output")
            .short('o')
            .long("output")
            .value_name("FILE")
            .help("输出文件"))
        .arg(Arg::new("verbose")
            .short('v')
            .long("verbose")
            .help("详细输出")
            .action(clap::ArgAction::SetTrue))
        .get_matches();
    
    // 获取参数值
    let input = matches.get_one::<String>("input").unwrap();
    let output = matches.get_one::<String>("output");
    let verbose = matches.get_flag("verbose");
    
    println!("输入: {}", input);
    if let Some(o) = output {
        println!("输出: {}", o);
    }
    if verbose {
        println!("详细模式");
    }
}

使用 derive 宏 (更简洁)

use clap::Parser;

#[derive(Parser, Debug)]
#[command(name = "myapp")]
#[command(about = "我的应用", long_about = None)]
struct Args {
    /// 输入文件
    #[arg(short, long)]
    input: String,
    
    /// 输出文件
    #[arg(short, long)]
    output: Option<String>,
    
    /// 详细输出
    #[arg(short, long, default_value_t = false)]
    verbose: bool,
    
    /// 端口号
    #[arg(short, long, default_value_t = 8080)]
    port: u16,
}

fn main() {
    let args = Args::parse();
    
    println!("输入: {}", args.input);
    if let Some(output) = args.output {
        println!("输出: {}", output);
    }
    println!("详细模式: {}", args.verbose);
    println!("端口: {}", args.port);
}

环境变量

读取环境变量

use std::env;

fn main() {
    // 获取单个环境变量
    match env::var("HOME") {
        Ok(home) => println!("HOME: {}", home),
        Err(e) => println!("无法获取 HOME: {}", e),
    }
    
    // 带默认值
    let port = env::var("PORT")
        .unwrap_or_else(|_| "8080".to_string());
    println!("端口: {}", port);
    
    // 解析为特定类型
    let debug = env::var("DEBUG")
        .unwrap_or_else(|_| "false".to_string())
        .parse::<bool>()
        .unwrap_or(false);
    println!("Debug: {}", debug);
    
    // 获取所有环境变量
    for (key, value) in env::vars() {
        println!("{}: {}", key, value);
    }
}

设置环境变量

use std::env;

fn main() {
    // 设置环境变量 (仅当前进程)
    env::set_var("MY_VAR", "my_value");
    println!("MY_VAR: {}", env::var("MY_VAR").unwrap());
    
    // 移除环境变量
    env::remove_var("MY_VAR");
    
    // 临时设置环境变量
    {
        let _guard = env::var("PATH").unwrap();
        env::set_var("PATH", "/custom/path");
        // 在此作用域内 PATH 为 /custom/path
    }
    // 作用域结束后原值恢复
}

使用 dotenv 加载 .env 文件

// 需要添加 dotenv 依赖
use dotenv::dotenv;
use std::env;

fn main() {
    // 从 .env 文件加载环境变量
    dotenv().ok();
    
    // 现在可以读取 .env 中的变量
    let database_url = env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");
    println!("数据库 URL: {}", database_url);
    
    let api_key = env::var("API_KEY")
        .expect("API_KEY must be set");
    println!("API Key: {}", api_key);
}

// .env 文件示例:
// DATABASE_URL=postgres://user:pass@localhost/db
// API_KEY=your-api-key-here
// DEBUG=true

线程与并发

创建线程

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

fn main() {
    // 创建线程
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("子线程: {}", i);
            thread::sleep(Duration::from_millis(100));
        }
    });
    
    // 主线程
    for i in 1..=5 {
        println!("主线程: {}", i);
        thread::sleep(Duration::from_millis(100));
    }
    
    // 等待线程结束
    handle.join().unwrap();
}

线程间通信

use std::sync::mpsc;
use std::thread;

fn main() {
    // 创建通道
    let (tx, rx) = mpsc::channel();
    
    // 发送者线程
    let sender = thread::spawn(move || {
        let messages = vec![
            "Hello",
            "from",
            "thread",
        ];
        
        for msg in messages {
            tx.send(msg).unwrap();
            thread::sleep(Duration::from_millis(100));
        }
    });
    
    // 主线程接收
    for received in rx {
        println!("收到: {}", received);
    }
    
    sender.join().unwrap();
}

共享状态

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 使用 Arc 和 Mutex 实现共享可变状态
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    
    // 等待所有线程
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("计数器: {}", *counter.lock().unwrap());
}

线程池

use std::thread;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};

// 简单线程池实现
struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    fn new(size: usize) -> ThreadPool {
        let (sender, receiver) = mpsc::channel();
        let receiver = Arc::new(Mutex::new(receiver));
        
        let mut workers = Vec::with_capacity(size);
        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }
        
        ThreadPool { workers, sender }
    }
    
    fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);
        self.sender.send(job).unwrap();
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();
            job();
        });
        
        Worker { id, thread }
    }
}

// 使用线程池
fn main() {
    let pool = ThreadPool::new(4);
    
    for i in 0..10 {
        pool.execute(move || {
            println!("任务 {} 在执行", i);
        });
    }
}

时间与日期

时间测量

use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

fn main() {
    // 测量代码执行时间
    let start = Instant::now();
    
    // 执行一些操作
    let mut sum = 0;
    for i in 0..1000000 {
        sum += i;
    }
    
    let duration = start.elapsed();
    println!("耗时: {:?}", duration);
    println!("微秒: {}", duration.as_micros());
    println!("毫秒: {}", duration.as_millis());
    println!("秒: {}", duration.as_secs());
    
    // 获取当前时间戳
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    println!("Unix 时间戳: {}", timestamp);
    
    // 创建 Duration
    let five_seconds = Duration::from_secs(5);
    let hundred_millis = Duration::from_millis(100);
    let one_microsecond = Duration::from_micros(1);
}

使用 chrono 库

// 需要添加 chrono 依赖
use chrono::{DateTime, Local, Utc, TimeZone, NaiveDate, NaiveTime, Duration};

fn main() {
    // 当前时间
    let now = Local::now();
    println!("当前时间: {}", now);
    println!("格式化: {}", now.format("%Y-%m-%d %H:%M:%S"));
    
    // UTC 时间
    let utc_now = Utc::now();
    println!("UTC 时间: {}", utc_now);
    
    // 从字符串解析
    let dt = NaiveDate::parse_from_str("2024-01-15", "%Y-%m-%d").unwrap();
    println!("日期: {}", dt);
    
    // 创建日期时间
    let dt = Local.with_ymd_and_hms(2024, 1, 15, 10, 30, 0).unwrap();
    println!("创建的日期时间: {}", dt);
    
    // 时间运算
    let tomorrow = Local::now() + Duration::days(1);
    let last_week = Local::now() - Duration::weeks(1);
    println!("明天: {}", tomorrow.format("%Y-%m-%d"));
    println!("上周: {}", last_week.format("%Y-%m-%d"));
    
    // 时间比较
    let dt1 = Local.with_ymd_and_hms(2024, 1, 15, 10, 0, 0).unwrap();
    let dt2 = Local.with_ymd_and_hms(2024, 1, 15, 11, 0, 0).unwrap();
    
    if dt1 < dt2 {
        println!("dt1 更早");
    }
    
    let diff = dt2.signed_duration_since(dt1);
    println!("时间差: {} 小时", diff.num_hours());
}

正则表达式

基本匹配

// 需要添加 regex 依赖
use regex::Regex;

fn main() {
    // 创建正则表达式
    let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
    
    // 测试是否匹配
    let date = "2024-01-15";
    if re.is_match(date) {
        println!("{} 是有效的日期格式", date);
    }
    
    // 查找匹配
    let text = "我的邮箱是 example@test.com 和 test@example.com";
    let email_re = Regex::new(r"\w+@\w+\.\w+").unwrap();
    
    for cap in email_re.captures_iter(text) {
        println!("邮箱: {}", &cap[0]);
    }
    
    // 查找所有匹配
    let phone_re = Regex::new(r"\d{3}-\d{4}").unwrap();
    let text = "电话: 123-4567, 987-6543";
    
    for mat in phone_re.find_iter(text) {
        println!("电话: {}", mat.as_str());
    }
}

替换和分组

use regex::Regex;

fn main() {
    // 替换
    let re = Regex::new(r"\d+").unwrap();
    let text = "我有 100 个苹果和 200 个橘子";
    let result = re.replace_all(text, "X");
    println!("替换后: {}", result);
    
    // 使用捕获组
    let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
    let text = "日期: 2024-01-15";
    
    if let Some(caps) = re.captures(text) {
        println!("年: {}", &caps[1]);
        println!("月: {}", &caps[2]);
        println!("日: {}", &caps[3]);
    }
    
    // 命名捕获组
    let re = Regex::new(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})").unwrap();
    let text = "2024-01-15";
    
    if let Some(caps) = re.captures(text) {
        println!("年: {}", &caps["year"]);
        println!("月: {}", &caps["month"]);
        println!("日: {}", &caps["day"]);
    }
    
    // 使用捕获组替换
    let result = re.replace(text, "$month/$day/$year");
    println!("美式日期: {}", result);
}

声明宏

// 基本宏定义
macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
}

// 带参数的宏
macro_rules! create_function {
    ($func_name:ident) => {
        fn $func_name() {
            println!("Function {:?} called", stringify!($func_name));
        }
    };
}

// 多参数宏
macro_rules! vec_macro {
    ($($x:expr),*) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

// 带类型的宏
macro_rules! hash_map {
    ($($key:expr => $value:expr),*) => {
        {
            let mut map = std::collections::HashMap::new();
            $(
                map.insert($key, $value);
            )*
            map
        }
    };
}

fn main() {
    say_hello!();
    
    create_function!(my_func);
    my_func();
    
    let v = vec_macro![1, 2, 3, 4, 5];
    println!("Vec: {:?}", v);
    
    let map = hash_map!["a" => 1, "b" => 2, "c" => 3];
    println!("Map: {:?}", map);
}

过程宏

// 自定义派生宏 (需要 proc-macro crate)
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    
    let name = &input.ident;
    let expanded = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    
    TokenStream::from(expanded)
}

// 使用
// #[derive(HelloMacro)]
// struct MyStruct;
// 
// MyStruct::hello_macro();

常用迭代器方法

映射与过滤

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // map: 转换每个元素
    let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    println!("加倍: {:?}", doubled);
    
    // filter: 过滤元素
    let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
    println!("偶数: {:?}", evens);
    
    // filter_map: 过滤并转换
    let result: Vec<i32> = numbers.iter()
        .filter_map(|x| if x % 2 == 0 { Some(x * 2) } else { None })
        .collect();
    println!("偶数加倍: {:?}", result);
    
    // take: 取前 n 个
    let first_three: Vec<&i32> = numbers.iter().take(3).collect();
    println!("前三个: {:?}", first_three);
    
    // skip: 跳过前 n 个
    let skip_five: Vec<&i32> = numbers.iter().skip(5).collect();
    println!("跳过前五个: {:?}", skip_five);
}

聚合操作

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // 求和
    let sum: i32 = numbers.iter().sum();
    println!("总和: {}", sum);
    
    // 求积
    let product: i32 = numbers.iter().product();
    println!("乘积: {}", product);
    
    // 最小/最大值
    let min = numbers.iter().min();
    let max = numbers.iter().max();
    println!("最小值: {:?}, 最大值: {:?}", min, max);
    
    // 计数
    let count = numbers.iter().count();
    println!("数量: {}", count);
    
    // 折叠 (fold)
    let sum_with_fold = numbers.iter().fold(0, |acc, x| acc + x);
    println!("Fold 求和: {}", sum_with_fold);
    
    // reduce (需要非空)
    let reduced = numbers.iter().reduce(|a, b| a + b);
    println!("Reduce: {:?}", reduced);
    
    // 判断条件
    let all_positive = numbers.iter().all(|x| *x > 0);
    let any_even = numbers.iter().any(|x| *x % 2 == 0);
    println!("全部为正: {}, 有偶数: {}", all_positive, any_even);
}

查找与排序

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // 查找第一个满足条件的元素
    let first_even = numbers.iter().find(|x| *x % 2 == 0);
    println!("第一个偶数: {:?}", first_even);
    
    // 查找元素的索引
    let position = numbers.iter().position(|x| *x == 5);
    println!("5 的位置: {:?}", position);
    
    // 排序
    let mut unsorted = vec![3, 1, 4, 1, 5, 9, 2, 6];
    unsorted.sort();
    println!("排序后: {:?}", unsorted);
    
    // 自定义排序
    unsorted.sort_by(|a, b| b.cmp(a));  // 降序
    println!("降序: {:?}", unsorted);
    
    // 稳定排序
    let mut pairs = vec![(2, 'b'), (1, 'a'), (2, 'a')];
    pairs.sort_by_key(|k| k.0);
    println!("按键排序: {:?}", pairs);
    
    // 反转
    let mut vec = vec![1, 2, 3, 4, 5];
    vec.reverse();
    println!("反转后: {:?}", vec);
    
    // 去重
    let mut duplicates = vec![1, 2, 2, 3, 3, 3, 4];
    duplicates.sort();
    duplicates.dedup();
    println!("去重后: {:?}", duplicates);
}

智能指针

Box 堆分配

// Box: 堆分配
let b = Box::new(5);
println!("Box: {}", b);

// 递归类型
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("{:?}", list);

Rc 引用计数

use std::rc::Rc;

fn main() {
    // Rc: 多所有权
    let a = Rc::new(5);
    let b = Rc::clone(&a);  // 增加引用计数
    let c = Rc::clone(&a);  // 增加引用计数
    
    println!("引用计数: {}", Rc::strong_count(&a));  // 3
    println!("值: {}", *b);  // 5
}

RefCell 内部可变性

use std::cell::RefCell;

fn main() {
    // RefCell: 运行时借用检查
    let value = RefCell::new(5);
    
    // 不可变借用
    let borrowed = value.borrow();
    println!("值: {}", *borrowed);
    drop(borrowed);  // 必须释放借用
    
    // 可变借用
    let mut mutable = value.borrow_mut();
    *mutable += 1;
    println!("修改后: {}", *mutable);
}

Arc + Mutex 多线程共享

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc + Mutex: 多线程共享可变数据
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("计数器: {}", *counter.lock().unwrap());
}

实用工具

随机数生成

// 需要添加 rand 依赖
use rand::Rng;
use rand::seq::SliceRandom;

fn main() {
    let mut rng = rand::thread_rng();
    
    // 随机整数
    let n1: i32 = rng.gen();
    let n2: i32 = rng.gen_range(0..100);
    println!("随机数: {}, {}", n1, n2);
    
    // 随机浮点数
    let f: f64 = rng.gen();
    println!("随机浮点数: {}", f);
    
    // 随机布尔值
    let b: bool = rng.gen();
    println!("随机布尔值: {}", b);
    
    // 从数组随机选择
    let choices = [1, 2, 3, 4, 5];
    let choice = choices.choose(&mut rng);
    println!("随机选择: {:?}", choice);
    
    // 打乱数组
    let mut nums = [1, 2, 3, 4, 5];
    nums.shuffle(&mut rng);
    println!("打乱后: {:?}", nums);
}

命令行颜色输出

// 需要添加 colored 依赖
use colored::Colorize;

fn main() {
    // 彩色文本
    println!("{}", "hello".red());
    println!("{}", "world".green().bold());
    println!("{}", "rust".blue().on_white());
    println!("{}", "colored".yellow().italic());
    println!("{}", "text".magenta().underline());
    
    // 组合样式
    let message = "Hello, Rust!";
    println!("{}", message.bright_cyan().on_black().bold());
}

进度条

// 需要添加 indicatif 依赖
use indicatif::ProgressBar;
use std::thread;
use std::time::Duration;

fn main() {
    let bar = ProgressBar::new(100);
    
    for _ in 0..100 {
        bar.inc(1);
        thread::sleep(Duration::from_millis(50));
    }
    
    bar.finish();
}

完整示例代码

所有代码片段均可直接复制使用。更多完整示例请参考:


返回: 目录

Rust 学习技能树

更新日期: 2026-04-04
当前版本: v0.1.0


学习路线

基础 (第 1-5 章) 🟢
       ↓
进阶 (第 6-10 章) 🟡
       ↓
高级 (第 11-15 章) 🔴
       ↓
实战项目 (第 16+ 章) 🔴

基础部分 🟢

前置要求:有基础编程概念(变量、循环、函数)

第 1 章:变量与表达式 ✅ 完成

  • 难度: 🟢 入门
  • 前置: 无
  • 预计时间: 1-2 小时
  • 练习: 3 个知识检查题
  • 检查点:
    • 理解 let 关键字
    • 区分可变/不可变变量
    • 了解 const

第 2 章:数据类型 ✅ 完成

  • 难度: 🟢 入门
  • 前置: 变量
  • 预计时间: 2-3 小时
  • 检查点:
    • 理解 i32, String, bool
    • 区分类与结构体
    • 了解 Vec

第 3 章:所有权 ✅ 完成

  • 难度: 🟡 中级
  • 前置: 变量、数据类型
  • 预计时间: 3-4 小时
  • 检查点:
    • 理解 move 语义
    • 会借用和引用
    • 识别所有权错误

第 4 章:结构体 ✅ 完成

  • 难度: 🟡 中级
  • 前置: 所有权、数据类型
  • 预计时间: 2-3 小时
  • 检查点:
    • 定义 struct
    • 实现方法
    • 使用字段

第 5 章:枚举 ✅ 完成

  • 难度: 🟡 中级
  • 前置: 结构体、所有权
  • 预计时间: 2-3 小时
  • 检查点:
    • 定义 enum
    • 使用 match
    • 理解 Option

进阶部分 🟡

前置要求: 完成基础部分

第 6 章:特征 (Traits) ✅ 完成

  • 难度: 🟡 中级
  • 前置: 结构体、枚举
  • 预计时间: 3-4 小时
  • 检查点:
    • 定义 trait
    • 实现特征
    • 使用特征作为参数

第 7 章:模块系统 ✅ 完成

  • 难度: 🟡 中级
  • 前置: 特征
  • 预计时间: 2-3 小时
  • 检查点:
    • 定义模块
    • 使用 use 导入
    • 控制可见性

第 8 章:泛型 ✅ 完成

  • 难度: 🟡 中级
  • 前置: 特征、结构体
  • 预计时间: 3-4 小时
  • 检查点:
    • 定义泛型函数
    • 理解单态化
    • 使用特征约束

第 9 章:闭包 ✅ 完成

  • 难度: 🟡 中级
  • 前置: 特征、函数
  • 预计时间: 2-3 小时
  • 检查点:
    • 定义闭包
    • 理解 Fn trait
    • 在迭代器中使用

第 10 章:线程与并发 🟡 进行中

  • 难度: 🔴 高级
  • 前置: 线程与并发
  • 预计时间: 4-5 小时
  • 检查点:
    • 创建线程
    • 使用通道
    • 理解 Arc/Mutex

高级部分 🔴

前置要求: 完成基础 + 进阶部分

第 11 章:条件编译 🟡 进行中

  • 难度: 🟡 中级
  • 前置: 模块
  • 预计时间: 1-2 小时

第 12 章:指针与 unsafe 🟡 进行中

  • 难度: 🔴 高级
  • 前置: 所有权
  • 预计时间: 3-4 小时
  • ⚠️ 注意:包含 unsafe 代码

第 13 章:日志记录 🟡 进行中

  • 难度: 🟡 中级
  • 前置: 特征、模块
  • 预计时间: 1-2 小时

第 14 章:追踪 (Tracing) 🟡 进行中

  • 难度: 🔴 高级
  • 前置: 日志记录
  • 预计时间: 2-3 小时

第 15 章:可见性控制 🟡 进行中

  • 难度: 🟡 中级
  • 前置: 模块
  • 预计时间: 1-2 小时

实战项目 🔴

前置要求: 完成进阶部分

项目 1:运行 gRPC 服务器

源码: src/bin/greeter_server.rs

运行:

cargo run --bin greeter_server

学习目标:

  • 理解 gRPC 基本概念
  • 实现协议 buffers 服务
  • 运行异步服务

扩展:

  • 添加自定义服务
  • 实现双向流

项目 2:IPC 通信

源码: src/bin/uds_server.rs, src/bin/uds_client.rs

运行:

# 在一个终端运行服务器
cargo run --bin uds_server

# 在另一个终端运行客户端
cargo run --bin uds_client

学习目标:

  • 理解 Unix 套接字
  • 实现进程间通信

项目 3:算法实现

源码: src/algo/calc_pi_sample.rs

运行:

cargo run --bin calc_pi  # 如果有此二进制
# 或
cd src/algo && rustc calc_pi_sample.rs && ./calc_pi_sample

学习目标:

  • 理解数值算法
  • 使用 rayon 并行

进度追踪

当前总进度: [X/15 章完成]

完成清单

  • 第 1 章:变量与表达式
  • 第 2 章:数据类型
  • 第 3 章:所有权
  • 第 4 章:结构体
  • 第 5 章:枚举
  • 第 6 章:特征
  • 第 7 章:模块系统
  • 第 8 章:泛型
  • 第 9 章:闭包
  • 第 10 章:线程与并发
  • 第 11 章:条件编译
  • 第 12 章:指针与 unsafe
  • 第 13 章:日志记录
  • 第 14 章:追踪
  • 第 15 章:可见性控制

完成 15 章后: 🎉 恭喜!你已经掌握了 Rust 的核心概念!

下一步:

  • 运行实战项目
  • 参与真实项目
  • 贡献开源代码

学习建议

每天学习时间

  • 初学者: 30-60 分钟/天
  • 有经验的开发者: 1-2 小时/天
  • 全职学习: 4-6 小时/天

🧠 基于认知科学的学习方法

Microsoft Rust 培训建议:"Struggling with the borrow checker is part of learning."

15 分钟规则

  1. 先自己写代码,让编译器报错
  2. 仔细阅读错误信息(Rust 编译器是最好的老师)
  3. 如果卡住超过 15 分钟 → 查看答案
  4. 关掉答案,从头自己写一遍

间隔重复

  • 学完一章后,第二天复习 前一章的关键概念
  • 每学完 5 章,做一次 综合复习(见复习章节)
  • 使用知识检查题测试自己的记忆

主动回忆

  • 不要只是阅读代码,自己写一遍
  • 合上教程,尝试凭记忆写出关键概念
  • 使用"费曼技巧":尝试向别人解释这个概念

最佳实践

  1. 边学边练 - 每章都要动手练习
  2. 做笔记 - 记录难点和收获
  3. 提问 - 在 RustCn 社区提问
  4. 复习 - 学完一章后复习前一章
  5. 编译器是你的老师 - 学会阅读错误信息

遇到困难时

  1. 回到前一章巩固基础
  2. 看代码示例(每个章节都有)
  3. 在 community 提问
  4. 休息后再试
  5. 记住:感到困惑是完全正常的! Rust 的学习曲线陡峭,但掌握后你会写出更可靠的代码。

参考资源

官方

中文社区

实践

知识检查题库

项目实战指南

更新日期: 2026-04-04
理念: 使用项目中实际存在的样例工程学习


项目原则

本系列教程使用 Hello Rust 项目中已有的代码作为教学示例,而不是虚构项目。

为什么?

  • ✅ 真实可运行的代码
  • ✅ 可以直接修改和试验
  • ✅ 与章节内容无缝衔接
  • ✅ 学完就能用在实际项目中

基础项目

1. Hello Rust 基础演示 🟢

文件: src/bin/basic.rs
代码量: ~15 行
难度: 🟢 入门

运行:

cargo run --bin basic

学习目标:

  • 熟悉 Rust 项目结构
  • 运行第一个 Rust 程序
  • 使用 cargo run 命令

动手试试:

#![allow(unused)]
fn main() {
// 修改 basic.rs 中的输出
println!("Hello, [你的名字]!");
}

进阶项目

2. gRPC 服务器示例 🟡

文件: src/bin/greeter_server.rs
代码量: ~100 行
难度: 🟡 中级

前置要求:

  • 理解异步编程
  • 了解 gRPC 基本概念

运行:

# 先安装 protoc
# macOS: brew install protobuf
# Linux: apt-get install protobuf-compiler

cargo run --bin greeter_server

学习重点:

  • gRPC 服务定义
  • Protocol Buffers
  • 异步服务实现

动手试试:

  1. 添加一个新的 RPC 方法
  2. 修改返回格式
  3. 添加日志输出

相关章节:


3. gRPC 客户端 🟡

文件: src/bin/greeter_client.rs
代码量: ~50 行
难度: 🟡 中级

运行:

# 确保服务器在运行
cargo run --bin greeter_server &

# 运行客户端
cargo run --bin greeter_client

学习重点:

  • gRPC 客户端调用
  • 错误处理
  • 连接管理

相关章节:


4. Unix Domain Socket IPC 🟡

文件: src/bin/uds_server.rs + src/bin/uds_client.rs
代码量: ~60 行
难度: 🟡 中级

运行:

# 在终端 1
cargo run --bin uds_server

# 在终端 2
cargo run --bin uds_client

学习重点:

  • Unix 套接字通信
  • 进程间通信 (IPC)
  • 错误处理

相关章节:


5. 标准输入输出 IPC 🟡

文件: src/bin/stdio_parent.rs + src/bin/stdio_child.rs
代码量: ~40 行
难度: 🟡 中级

运行:

cargo run --bin stdio_parent

学习重点:

  • 子进程创建
  • 管道通信
  • Stdin/Stdout 处理

相关章节:


算法项目

6. PI 值计算 🟡

文件: src/algo/calc_pi_sample.rs
代码量: ~100 行
难度: 🟡 中级

运行:

cd src/algo
rustc calc_pi_sample.rs -o calc_pi
./calc_pi

学习重点:

  • 数值算法
  • 循环和迭代
  • 精度计算

相关章节:


7. LeetCode 题解 🟢

文件: crates/leetcode/src/
代码量: ~50 行/题
难度: 🟢 入门

题目列表:

  1. 两数之和 - HashMap 应用
  2. 两数相加 - 链表操作

运行:

cd crates/leetcode
cargo test

学习重点:

  • 数据结构应用
  • 算法实现
  • 测试驱动

相关章节:


框架实战

8. Awesome 框架应用 🔴

目录: crates/awesome/src/
代码量: ~2000 行
难度: 🔴 高级

包含:

  • 服务生命周期管理
  • 依赖注入
  • 数据库连接池
  • gRPC 服务

学习重点:

  • 生产级架构
  • 设计模式
  • 错误处理最佳实践

相关章节:


项目完成清单

基础阶段

    1. Hello Rust 基础演示
    1. 运行所有示例

进阶阶段

    1. gRPC 服务器
    1. gRPC 客户端
    1. UDS IPC
    1. Stdio IPC

算法阶段

    1. PI 值计算
    1. LeetCode 两数之和
    1. LeetCode 两数相加

框架阶段

    1. Awesome 框架概览
    1. 实现自定义服务
    1. 数据库集成实战

学习建议

项目练习流程

  1. 阅读相关章节 - 先学习理论知识
  2. 运行示例代码 - 确认环境正常
  3. 修改代码试验 - 试试改动有什么效果
  4. 独立完成扩展 - 按练习建议实现功能

遇到问题时

  1. 查看章节中的"常见错误"
  2. 搜索错误信息
  3. RustCN 论坛 提问
  4. 查看其他项目示例

进阶路径

基础项目 → 进阶项目 → 算法项目 → 框架实战 → 贡献代码

贡献

欢迎贡献更多项目示例!

提交 PR 前确保:

  • 代码可编译运行
  • 添加相关文档
  • 通过测试
  • 符合项目风格

下一步: 选择一个项目开始吧!🎯

项目实战:CLI 待办事项管理器

难度: 🟡 中级
代码量: ~170 行
涉及知识点: clap 参数解析、serde 序列化、anyhow 错误处理、文件系统操作


项目目标

构建一个支持增删改查的 CLI 待办事项工具,数据持久化到 JSON 文件。


技术栈

Crate用途
clap (derive)CLI 参数解析
serde + serde_jsonJSON 数据持久化
anyhow错误处理(带上下文)
chrono时间戳
dirs获取用户主目录

项目结构

examples/todo/
├── Cargo.toml
├── .gitignore
├── src/
│   └── main.rs        # 主程序
└── tests/
    ├── common.rs      # 测试工具类
    └── todo_test.rs   # 集成测试 (8 个测试)

核心设计

1. CLI 参数解析 (clap)

使用 clap 的 derive API 定义命令结构:

#![allow(unused)]
fn main() {
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "todo")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Add a new todo
    Add { description: String },
    /// List all todos
    List,
    /// Mark a todo as done
    Done { id: usize },
    /// Delete a todo
    Delete { id: usize },
}
}

关键知识点:

  • #[derive(Parser)]: 自动生成参数解析代码
  • #[command(subcommand)]: 定义子命令
  • /// 注释: 自动生成 --help 文本

2. 数据模型 (serde)

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Todo {
    id: usize,
    description: String,
    done: bool,
    created_at: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct TodoStore {
    todos: Vec<Todo>,
    next_id: usize,
}
}

关键知识点:

  • #[derive(Serialize, Deserialize)]: 自动生成序列化代码
  • next_id: 自增 ID,避免删除后 ID 冲突

3. 数据持久化

#![allow(unused)]
fn main() {
impl TodoStore {
    fn load(path: &PathBuf) -> Result<Self> {
        if path.exists() {
            let data = fs::read_to_string(path)
                .with_context(|| format!("Failed to read {}", path.display()))?;
            let store: TodoStore = serde_json::from_str(&data)
                .with_context(|| "Failed to parse todo data")?;
            Ok(store)
        } else {
            Ok(TodoStore::new())
        }
    }

    fn save(&self, path: &PathBuf) -> Result<()> {
        let data = serde_json::to_string_pretty(self)
            .with_context(|| "Failed to serialize todo data")?;
        fs::write(path, data)
            .with_context(|| format!("Failed to write {}", path.display()))?;
        Ok(())
    }
}
}

关键知识点:

  • with_context(): 添加错误上下文信息
  • to_string_pretty(): 格式化 JSON 输出

4. 错误处理 (anyhow)

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let cli = Cli::parse();
    let data_path = get_data_path();
    let mut store = TodoStore::load(&data_path)?;

    match cli.command {
        Commands::Done { id } => {
            store.mark_done(id)
                .with_context(|| format!("Failed to mark todo {} as done", id))?;
            // ...
        }
        // ...
    }

    Ok(())
}

使用示例

# 添加待办
cargo run -- add "Learn Rust basics"
cargo run -- add "Build a CLI app"
cargo run -- add "Read Rust Book"

# 列出所有待办
cargo run -- list

# 输出:
# ID    Done  Description                    Created At
# ---------------------------------------------------------------------------
# 1     ⬜     Learn Rust basics              2026-04-05 15:00:12
# 2     ⬜     Build a CLI app                2026-04-05 15:00:13
# 3     ⬜     Read Rust Book                 2026-04-05 15:00:13

# 标记完成
cargo run -- done 1

# 删除待办
cargo run -- delete 2

# 查看帮助
cargo run -- --help

测试

项目包含 8 个集成测试,覆盖所有核心功能:

cd examples/todo
cargo test

测试覆盖:

  • ✅ 添加待办
  • ✅ 列出待办
  • ✅ 标记完成
  • ✅ 删除待办
  • ✅ 错误处理(不存在的 ID)
  • ✅ 空列表处理
  • ✅ 数据持久化

CLI 集成测试

本项目使用 进程级集成测试 模式,通过 std::process::Command 启动真实的 CLI 进程进行测试,确保端到端功能正确。

测试架构

tests/
├── common.rs      # 测试工具类 (TodoTest, TodoOutput)
└── todo_test.rs   # 8 个集成测试用例

核心组件

1. TodoTest 测试工具类 (common.rs)

#![allow(unused)]
fn main() {
pub struct TodoTest {
    pub temp_dir: TempDir,       // 临时目录隔离测试
    pub cwd: PathBuf,
    pub env: HashMap<String, String>,
}

impl TodoTest {
    pub fn new() -> Self {
        let temp_dir = TempDir::new().expect("Failed to create temp dir");
        let home_path = temp_dir.path().to_str().unwrap().to_string();
        
        let mut test = Self {
            temp_dir,
            cwd: temp_dir.path().into(),
            env: HashMap::new(),
        };
        
        // 重定向 HOME 到临时目录,隔离测试数据
        test.env.insert("HOME".into(), home_path);
        test
    }

    pub fn todo(&self, args: &[&str]) -> TodoOutput {
        let mut cmd = Command::new(env!("CARGO_BIN_EXE_todo"));
        cmd.current_dir(&self.cwd);
        cmd.env_clear().envs(&self.env);  // 清理环境变量
        cmd.args(args);
        
        let output = cmd.output().expect("Failed to execute");
        TodoOutput::new(output)
    }
}
}

关键设计:

  • TempDir: 每个测试使用独立临时目录,测试结束后自动清理
  • env_clear(): 清除环境变量,避免测试间相互影响
  • HOME 重定向: 将数据存储路径指向临时目录

2. TodoOutput 断言工具 (common.rs)

#![allow(unused)]
fn main() {
pub struct TodoOutput {
    pub output: std::process::Output,
}

impl TodoOutput {
    #[track_caller]
    pub fn assert_success(&self) -> &Self {
        assert!(self.output.status.success(),
            "Expected success, got:\n# STDERR\n{}\n# STDOUT\n{}",
            String::from_utf8_lossy(&self.output.stderr),
            String::from_utf8_lossy(&self.output.stdout)
        );
        self
    }

    #[track_caller]
    pub fn assert_stdout_contains(&self, expected: &str) -> &Self {
        let stdout = self.stdout();
        assert!(stdout.contains(expected),
            "Expected stdout to contain '{}', got:\n{}",
            expected, stdout
        );
        self
    }
}
}

关键设计:

  • #[track_caller]: 断言失败时显示测试代码行号,而非断言工具行号
  • 链式调用: 支持 .assert_success().assert_stdout_contains("...") 链式断言

测试用例示例

#![allow(unused)]
fn main() {
#[test]
fn test_mark_todo_done() {
    let test = TodoTest::new();
    
    // 添加待办
    test.todo(&["add", "Learn Rust"]).assert_success();
    
    // 标记完成
    test.todo(&["done", "1"])
        .assert_success()
        .assert_stdout_contains("Marked todo #1 as done");
    
    // 验证状态
    test.todo(&["list"])
        .assert_stdout_contains("✅");
}

#[test]
fn test_multiple_todos_persistence() {
    let test = TodoTest::new();
    
    // 添加多个待办
    test.todo(&["add", "Task 1"]).assert_success();
    test.todo(&["add", "Task 2"]).assert_success();
    test.todo(&["done", "1"]).assert_success();
    
    // 验证数据持久化
    test.todo(&["list"])
        .assert_stdout_contains("Task 1")
        .assert_stdout_contains("Task 2")
        .assert_stdout_contains("✅");
}
}

运行测试

cd examples/todo
cargo test

测试输出:

running 8 tests
test test_add_todo ... ok
test test_list_empty ... ok
test test_delete_nonexistent_todo ... ok
test test_done_nonexistent_todo ... ok
test test_delete_todo ... ok
test test_mark_todo_done ... ok
test test_list_todos ... ok
test test_multiple_todos_persistence ... ok

test result: ok. 8 passed; 0 failed

测试覆盖矩阵

测试用例覆盖功能断言点
test_add_todo添加待办成功消息
test_list_todos列出待办内容显示
test_mark_todo_done标记完成状态变更
test_delete_todo删除待办删除确认
test_done_nonexistent_todo错误处理失败状态
test_delete_nonexistent_todo错误处理失败状态
test_list_empty边界条件空列表提示
test_multiple_todos_persistence数据持久化状态保持

相关章节


扩展挑战

  • 添加优先级和截止日期
  • 支持分类标签
  • 添加单元测试覆盖率达到 80%+
  • 支持编辑待办描述
  • 添加过滤功能(按状态、标签)

简易 HTTP 服务器

多线程爬虫

IPC 与分布式示例

本部分涵盖项目中 src/bin/ 目录下的 15 个二进制示例,包括 gRPC 服务、Unix Domain Socket 通信、标准输入输出 IPC 和进程控制。


gRPC 示例

Hello gRPC 服务

文件: grpc_hello_server.rs (18 行)

cargo run --bin grpc_hello_server

学习目标:

  • 使用 tonic 构建 gRPC 服务器
  • 定义 Protocol Buffer 服务
  • 实现 RPC 方法

Hello gRPC 客户端

文件: grpc_hello_client.rs (13 行)

cargo run --bin grpc_hello_client

学习目标:

  • 连接 gRPC 服务器
  • 发送 RPC 请求
  • 处理响应

Greeter 服务

文件: greeter_server.rs (98 行)

文件: greeter_client.rs (59 行)

# 先启动服务器
cargo run --bin greeter_server

# 再启动客户端
cargo run --bin greeter_client

学习目标:

  • 完整的 gRPC 服务实现
  • 请求/响应模式
  • 错误处理

gRPC Store 服务

文件: grpc_store_server.rs (19 行)

文件: grpc_store_client.rs (145 行)

cargo run --bin grpc_store_server
cargo run --bin grpc_store_client

学习目标:

  • 状态ful gRPC 服务
  • 键值存储模式
  • 复杂 RPC 交互

Unix Domain Socket (UDS) 示例

UDS 服务器

文件: uds_server.rs (62 行)

cargo run --bin uds_server

学习目标:

  • Unix Domain Socket 服务器
  • 本地进程间通信
  • 异步接受连接

UDS 客户端

文件: uds_client.rs (42 行)

cargo run --bin uds_client

学习目标:

  • 连接 UDS 服务器
  • 发送/接收消息

UDS 父进程

文件: uds_parent.rs (40 行)

cargo run --bin uds_parent

学习目标:

  • 父进程创建 UDS
  • 管理子进程通信

标准输入输出 IPC 示例

Stdio 父进程

文件: stdio_parent.rs (38 行)

cargo run --bin stdio_parent

学习目标:

  • 生成子进程
  • 通过 stdin/stdout 通信
  • 等待子进程完成

Stdio 子进程

文件: stdio_child.rs (20 行)

# 通常由 stdio_parent 启动
cargo run --bin stdio_child

学习目标:

  • 从 stdin 读取
  • 向 stdout 写入
  • 作为子进程运行

进程控制示例

系统控制 (SysCtl)

文件: app_sys_ctl.rs (286 行)

cargo run --bin app_sys_ctl

学习目标:

  • 系统管理工具
  • 进程生命周期管理
  • 信号处理

Nix 控制 (NixCtl)

文件: app_nix_ctl.rs (282 行)

cargo run --bin app_nix_ctl

学习目标:

  • 使用 nix crate 进行系统调用
  • 进程组和会话管理
  • 守护进程模式

基础演示

Basic 演示

文件: basic.rs (22 行)

cargo run --bin basic

学习目标:

  • 运行第一个 Rust 程序
  • 理解 main 函数

Advance 演示

文件: advance.rs (90 行)

cargo run --bin advance

学习目标:

  • 调用基础/进阶示例
  • 理解模块调用

运行所有示例

# 列出所有可用的二进制
cargo build --bins

# 运行特定二进制
cargo run --bin <name>

# 运行所有测试
cargo test --workspace

相关章节

💡 提示: UDS 示例仅在 Unix 系统上可用。Windows 用户可以使用 stdio IPC 示例。

贡献指南

术语表 (Glossary)

Purpose: Bilingual terminology reference (中文 ↔ English) for consistent translation across all documentation chapters

Created: 2026-04-03
Branch: 001-rust-tutorial-docs
Usage: Reference this glossary when writing chapters to ensure terminology consistency


核心概念 (Core Concepts)

English中文First Use FormatNotes
Ownership所有权所有权 (ownership)Rust's core resource management concept
Borrowing借用借用 (borrowing)Temporary access to data without taking ownership
Lifetime生命周期生命周期 (lifetime)Scope for which a reference is valid
Reference引用引用 (reference)Pointer to data without ownership
Move移动移动 (move)Transfer of ownership
Copy复制复制 (copy)Bitwise copy of data (for Copy types)
Clone克隆克隆 (clone)Deep copy operation
Scope作用域作用域 (scope)Range of code where variable is valid
Variable变量变量 (variable)Named storage location
Binding绑定绑定 (binding)Association between name and value
Expression表达式表达式 (expression)Code that evaluates to a value
Statement语句语句 (statement)Code that performs an action
Function函数函数 (function)Reusable code block
Method方法方法 (method)Function associated with type
Parameter参数参数 (parameter)Function input variable
Argument实参实参 (argument)Actual value passed to function

数据类型 (Data Types)

English中文First Use FormatNotes
Type类型类型 (type)Classification of data
Scalar标量标量 (scalar)Single value type
Compound复合复合 (compound)Multiple values type
Integer整数整数 (integer)Whole number type
Floating Point浮点数浮点数 (floating point)Decimal number type
Boolean布尔值布尔值 (boolean)true/false type
Character字符字符 (character)Single Unicode character
String字符串字符串 (string)Text type
Array数组数组 (array)Fixed-size collection
Slice切片切片 (slice)Dynamic view into collection
Tuple元组元组 (tuple)Fixed-size heterogeneous collection
Struct结构体结构体 (struct)Custom composite type
Enum枚举枚举 (enum)Type with named variants
Union联合体联合体 (union)Overlapping data representation
Option选项类型OptionType representing optional value
Result结果类型Result<T, E>Type representing success/error
Vector向量VecGrowable array
HashMap哈希映射HashMap<K, V>Key-value map
Box盒子BoxHeap pointer type
Rc引用计数RcReference-counted pointer
Arc原子引用计数ArcAtomic reference-counted pointer

特征与泛型 (Traits & Generics)

English中文First Use FormatNotes
Trait特征特征 (trait)Interface definition
Generic泛型泛型 (generic)Type parameterization
Type Parameter类型参数类型参数 (type parameter)Generic type placeholder
Implement实现实现 (implement)Provide trait definition
Inference推断推断 (inference)Automatic type deduction
Annotation注解注解 (annotation)Explicit type declaration
Constraint约束约束 (constraint)Generic type limitation
Bound边界边界 (bound)Trait requirement on generic
Default默认默认 (default)Fallback implementation
Derive派生派生 (derive)Automatic trait implementation
Macro宏 (macro)Code generation facility
Attribute属性属性 (attribute)Metadata annotation
Procedural Macro过程宏过程宏 (procedural macro)Compile-time code generation
Declarative Macro声明宏声明宏 (declarative macro)Pattern-based code generation

内存管理 (Memory Management)

English中文First Use FormatNotes
Stack栈 (stack)LIFO memory region
Heap堆 (heap)Dynamic memory region
Allocation分配分配 (allocation)Memory reservation
Deallocation释放释放 (deallocation)Memory return
Drop丢弃Drop traitResource cleanup trait
Destructor析构函数析构函数 (destructor)Cleanup function
Dangling悬垂悬垂指针 (dangling pointer)Invalid reference
Leak泄漏内存泄漏 (memory leak)Unreturned memory
Safe安全安全 (safe)Memory-safe operation
Unsafe不安全不安全 (unsafe)Requires manual safety proof

并发与异步 (Concurrency & Async)

English中文First Use FormatNotes
Concurrency并发并发 (concurrency)Multiple tasks in progress
Parallelism并行并行 (parallelism)Multiple tasks simultaneous
Thread线程线程 (thread)Execution unit
Async/Await异步/等待async/awaitAsynchronous programming model
Future未来值FutureAsync computation result
Executor执行器执行器 (executor)Async task scheduler
Runtime运行时运行时 (runtime)Execution environment
TokioTokioTokioAsync runtime crate
Mutex互斥锁MutexMutual exclusion primitive
RwLock读写锁RwLockRead-write lock
Channel通道通道 (channel)Message passing conduit
Send发送Send traitThread-safe transfer trait
Sync同步Sync traitThread-shared trait
Atomic原子操作原子操作 (atomic)Indivisible operation

错误处理 (Error Handling)

English中文First Use FormatNotes
Error错误错误 (error)Failure condition
Panic恐慌panic!Unrecoverable error
Unwind展开栈展开 (stack unwind)Stack cleanup on panic
Abort中止中止 (abort)Immediate termination
Expect预期expect()Unwrap with message
Unwrap解包unwrap()Extract value or panic
Match匹配matchPattern matching expression
Propagate传播传播 (propagate)Pass error to caller
Handle处理处理 (handle)Manage error condition
Recoverable可恢复可恢复错误 (recoverable error)Handled failure
Unrecoverable不可恢复不可恢复错误 (unrecoverable error)Fatal failure

工具与生态系统 (Tools & Ecosystem)

English中文First Use FormatNotes
CargoCargoCargoRust package manager
Crate箱 (crate)Compilation unit
Package包 (package)Distribution unit
Module模块模块 (module)Code organization unit
Workspace工作空间工作空间 (workspace)Multi-crate project
Dependency依赖依赖 (dependency)External crate requirement
Feature特性特性 (feature)Optional functionality
Build构建构建 (build)Compilation process
Test测试测试 (test)Verification process
Lint代码检查代码检查 (lint)Code quality check
ClippyClippyClippyRust linter tool
RustfmtRustfmtrustfmtCode formatter
Doc文档文档 (documentation)Generated documentation
Benchmark基准测试基准测试 (benchmark)Performance measurement
Profile性能分析性能分析 (profiling)Performance analysis

常用短语 (Common Phrases)

English中文Usage Context
Compile time编译时When code is compiled
Runtime运行时When code executes
Type safety类型安全Prevention of type errors
Memory safety内存安全Prevention of memory errors
Zero-cost abstraction零成本抽象No runtime overhead
Fearless concurrency无畏并发Safe concurrent programming
Data race数据竞争Concurrent memory access bug
Undefined behavior未定义行为Unpredictable program behavior
Borrow checker借用检查器Compiler ownership validator

Usage Guidelines

For Chapter Writers:

  1. First occurrence in chapter: Always use format 中文 (English)

    • ✅ Correct: "所有权 (ownership) 是 Rust 的核心概念"
    • ✅ Correct: "让我们了解 borrowing (借用) 的规则"
    • ❌ Wrong: "Ownership 是重要的" (English without translation)
    • ❌ Wrong: "所有权是重要的" (No English reference for searchability)
  2. Subsequent occurrences: Use Chinese only

    • ✅ "所有权系统确保..."
    • ✅ "借用规则防止..."
  3. Code examples: Keep English keywords

    • let x = 5; (not 让 X = 5;)
    • fn main() (not 函数 主 ())
  4. Links to external docs: Use English terms for URLs

    • ✅ Link to std::string::String (not std::字符串::字符串)

Glossary Maintenance:

  • Add new terms as discovered during chapter writing
  • Ensure all contributors use this single source of truth
  • Update if Rust community adopts new Chinese translations

References

常见问题 FAQ

更新日志