About Hello Rust
一个编程高手是怎样练成的呢? 惟手熟尔。重在刻意练习。 这意为着,就是不断重复练习,实践,再实践,熟练掌握各种技能。因为,只有反复练习,才能真正掌握。
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 程序吧。
单元测试的结构
单元测试通常包含以下部分:
- 导入模块:使用
use语句导入需要的模块。 - 定义测试函数:使用
#[test]注解定义测试函数。测试函数应该以fn开头,并且返回Result或Option类型。 - 编写测试代码:在测试函数中编写实际的测试代码。你可以使用断言来验证函数的行为。
- 运行测试:使用
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 的核心概念和编程范式。这些知识是后续所有高级主题的基石。
🎯 你将学到什么
完成本部分学习后,你将能够:
- 理解 Rust 所有权系统 - Rust 最核心、最独特的概念
- 掌握借用和生命周期 - 安全地共享数据
- 使用结构体和枚举 - 组织复杂数据
- 编写泛型和特征 - 实现可复用的代码
- 理解并发基础 - 安全的多线程编程
📚 章节列表
| 章节 | 说明 | 难度 | 预计时间 |
|---|---|---|---|
| 变量与表达式 | 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 模块系统
- 使用闭包捕获环境
- 创建基本的多线程程序
🎓 实践项目
建议练习:
- 创建一个简单的命令行工具
- 实现一个数据结构(如待办事项列表)
- 编写多线程程序计算数据
➡️ 下一步
完成基础入门后,继续学习 高级进阶 部分,你将学习:
- 异步编程 (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 模块系统
- 使用闭包捕获环境
- 创建基本的多线程程序
🎓 实践项目
建议练习:
- 创建一个简单的命令行工具
- 实现一个数据结构(如待办事项列表)
- 编写多线程程序计算数据
➡️ 下一步
完成基础入门后,继续学习 高级进阶 部分,你将学习:
- 异步编程 (Async/Await)
- 数据库操作 (SQLx, Diesel)
- Web 框架 (Axum, Hyper)
- 序列化 (Serde, JSON)
- 以及更多高级主题!
准备好了吗?让我们开始 变量与表达式 的学习! 🚀
知识检查
快速测验(答案在下方):
-
Rust 是什么类型的语言?
-
Rust 的主要优势是什么?
-
本教程的学习路径是什么?
点击查看答案与解析
- Rust 是系统级编程语言,注重内存安全和性能
- 内存安全无需 GC、零成本抽象、并发安全
- 基础入门 → 高级进阶 → 实战精选 → 算法练习
关键理解: Rust 是一门独特的语言,结合了系统级性能和现代语言安全性。
延伸阅读
完成基础入门后,你可能还想了解:
- Rust 官方 Book - 最权威的学习资源
- Rust By Example - 代码驱动学习
- Rustlings 练习 - 交互式练习
选择建议:
变量与表达式
开篇故事
想象你有一个工具箱,里面装着各种工具:螺丝刀、锤子、尺子。你给每个工具贴上标签,下一次需要时就知道去哪里找。Rust 中的变量就像这些贴标签的工具箱 - 它们帮你存储和管理程序中的数据。表达式则是你使用这些工具完成的工作。
本章适合谁
如果你是 Rust 初学者,想理解如何存储数据、进行计算和控制程序流程,本章适合你。这是所有编程的基础,即使你是第一次接触编程也能理解。
你会学到什么
完成本章后,你可以:
- 使用
let关键字声明和初始化变量 - 理解不可变性(immutability)和可变性(mutability)的区别
- 区分变量和常量
- 使用表达式进行数值和位运算
- 编写条件语句和循环表达式
前置要求
本章是 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 要这样设计?
- 安全性:防止意外的数据修改
- 并发安全:不可变数据可以安全地在线程间共享
- 更容易推理:知道值不会改变,代码更容易理解
类比:
就像你写在纸上的数字 - 写完后就不能改变。如果你想改,需要拿一张新纸重写。
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) |
|---|---|---|
| 关键字 | let | const |
| 类型注解 | 可选 | 必须 |
| 编译时确定 | 否 | 是 |
| 生命周期 | 作用域内 | 整个程序运行期间 |
| 内存地址 | 运行时分配 | 编译时嵌入代码 |
| 可以使用函数值 | ✅ 是 | ❌ 否(只能用字面量) |
何时使用常量:
- 配置值(最大用户数、超时时间)
- 数学常数(π, 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
修复方法:
-
如果真的需要修改,添加
mut:#![allow(unused)] fn main() { let mut x = 5; x = 10; // ✅ 现在可以了 } -
如果不需要修改,接受不可变性:
#![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
解析:
x = 5- 第一次绑定x = 5 + 1 = 6- 遮蔽,新 x 是 6- 内部作用域:
x = 6 * 2 = 12- 再次遮蔽 - 内部作用域结束,内部 x 失效
- 外部 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 代码更简洁、更具表现力。
小结
核心要点:
- 变量默认不可变 - 这是 Rust 的安全特性
- 使用
mut声明可变变量 - 只在需要时 - 常量用
const定义 - 必须标注类型,全大写命名 - 遮蔽允许复用名称 - 可以改变类型,比
mut更灵活 - Rust 大多数结构是表达式 - 返回值,不以分号结尾
关键术语:
- Binding (绑定): 将名称关联到值
- Immutable (不可变): 不能修改的值
- Mutable (可变): 可以修改的值
- Shadowing (遮蔽): 用相同名称声明新变量
- Expression (表达式): 返回值的代码
- Statement (语句): 不返回值的代码动作
下一步:
术语表
| English | 中文 |
|---|---|
| Variable | 变量 |
| Constant | 常量 |
| Immutable | 不可变 |
| Mutable | 可变 |
| Binding | 绑定 |
| Expression | 表达式 |
| Statement | 语句 |
| Shadowing | 遮蔽 |
| Type annotation | 类型注解 |
完整示例:src/basic/expression_sample.rs
延伸阅读
学习完变量与表达式后,你可能还想了解:
let关键字 - 变量绑定深入- Rust 表达式语法 - 官方参考
- 常量 vs 变量 - 何时使用
const
选择建议:
继续学习
💡 记住:不可变性是 Rust 的默认设置 - 如果你不特别告诉它"这个要改变",Rust 会让它保持不变。这是为了你的安全!
💡 小知识:为什么 Rust 变量默认不可变?
历史教训: 在 C/C++ 中,意外修改变量是常见 bug 来源:
// C 语言示例
int calculate(int x) {
x = x * 2; // 😱 意外修改了参数
return x;
}
Rust 的设计哲学:
"如果你需要修改,请明确说出来"
这叫做默认不可变 (immutable by default),好处是:
- 编译器能帮你发现错误 - 意外修改会报编译错误
- 更容易推理代码 - 知道值不会变
- 并发更安全 - 不可变数据可以在线程间安全共享
试试这个:
// 猜猜哪行会报错?
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 X 和let y 都会在编译时计算
解析: const 强制编译时求值,字面量表达式也会被编译器优化。static 在运行时初始化。
函数
开篇故事
想象你在组装乐高积木。每次需要搭建一个小房子时,你都要重新看说明书、找积木、一块块拼接——这既耗时又容易出错。但如果有一个"房子制作器"机器,你只需放入积木,按下按钮,房子就出来了!这就是函数的核心思想:将重复的逻辑封装起来,随时调用。
在 Rust 中,函数是代码的基本构建块。通过函数,你可以将复杂的问题分解为小的、可管理的部分,让代码更易读、可复用。
本章适合谁
如果你已经学完了变量和表达式,现在想学习如何组织代码、避免重复,本章适合你。函数是编程的基础,无论你是什么水平的开发者,都会频繁使用函数。
你会学到什么
完成本章后,你可以:
- 使用
fn关键字定义函数 - 理解参数和返回值的语法
- 区分表达式和语句
- 理解所有权的转移和借用
- 使用元组返回多个值
前置要求
学习本章前,你需要理解:
第一个例子
让我们看一个最简单的函数定义:
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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 函数定义 | def add(a, b): | int add(int a, int b) | int add(int a, int b) | fn add(a: i32, b: i32) -> i32 | Rust 必须标注参数类型 |
| 返回值 | return a + b | return a + b; | return a + b; | a + b (无分号) 或 return | Rust 支持隐式返回 |
| 多返回值 | 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 }
小结
核心要点:
- 函数定义:
fn name(params) -> ReturnType { body } - 参数类型: 必须标注类型,使用
name: Type格式 - 返回值: 最后一个表达式(无分号)或使用
return - 所有权: 参数默认获取所有权,使用
&借用 - 元组返回: 使用
(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 - 泛型函数示例
知识检查
快速测验(答案在下方):
- 这段代码能编译通过吗?为什么?
fn add(a: i32, b: i32) -> i32 {
a + b;
}
-
函数参数默认是可变还是不可变?
-
如何返回多个值?
点击查看答案与解析
- ❌ 不能 - 返回值有分号,返回
()而不是i32 - 不可变 - 需要使用
mut关键字 - 使用元组:
fn foo() -> (i32, String) { (1, "hello".to_string()) }
关键理解: Rust 函数返回值是最后一个表达式(无分号)。
继续学习
💡 记住:函数是代码的基石。好的函数应该短小、专注、可复用!
基础数据类型
开篇故事
想象你在整理一个工具箱。你会把螺丝刀、锤子、尺子放在不同的格子里,因为每种工具用途不同。Rust 的数据类型就像这些格子——它们告诉编译器每个值应该如何存储和使用。
本章适合谁
如果你已经学完了变量与表达式,现在想了解 Rust 有哪些数据类型以及如何使用它们,本章适合你。数据类型是编程的基础,理解它们对编写正确的代码至关重要。
你会学到什么
完成本章后,你可以:
- 区分标量类型和复合类型
- 使用整数、浮点数、布尔值和字符类型
- 理解 String 和 &str 的区别
- 使用元组、数组、Vec 和 HashMap
- 进行类型转换和类型推断
前置要求
- 变量与表达式 - 变量绑定和可变性
第一个例子
最简单的数据类型声明:
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-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| 架构相关 | isize | usize |
#![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(最常用) - 需要大数:使用
i64或i128 - 索引/计数:使用
usize - 节省内存:使用
i8或i16
知识扩展
日期与时间
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 | 键值对,有序 | 有序数据 |
小结
核心要点:
- 标量类型: i32, f64, bool, char - 单个值
- 复合类型: 元组、数组、Vec、HashMap - 多个值
- String vs &str: 可变 vs 不可变
- 类型转换: 使用
as进行显式转换 - 集合选择: 根据需求选择合适的数据结构
关键术语:
- 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 与其他语言最大的不同之处,需要多花些时间理解——这完全正常。
你会学到什么
完成本章后,你可以:
- 解释什么是所有权以及为什么 Rust 需要它
- 理解值何时被移动 (move) 以及移动的后果
- 识别所有权转移的代码模式
- 避免"移动后使用"的常见错误
- 理解如何正确返回函数内部创建的数据
前置要求
学习本章前,你需要理解:
第一个例子
让我们看一个最简单的所有权示例:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; println!("{}", s2); // ✅ 可以工作 // println!("{}", s1); // ❌ 编译错误!s1 已经移动给 s2 了 }
发生了什么?
第 2 行 let s2 = s1; 不是复制字符串,而是转移所有权。s1 的所有权移动给了 s2,s1 不再有效。
Python/Java/C++ vs Rust 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | 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 的所有权系统遵循三条简单规则:
-
每个值都有一个所有者
- 变量是值的所有者
- 所有者负责清理值
-
任一时刻只能有一个所有者
- 不像其他语言可以有多个引用指向同一数据
- Rust 确保内存安全
-
所有者离开作用域,值被丢弃
- 自动清理,无需手动释放
- 防止内存泄漏
移动语义 (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 已经移动给函数了
修复方法:
-
使用引用传递(推荐):
#![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 仍然可用 } -
返回所有权:
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 在函数栈上创建,函数结束时会被清理。返回指向它的引用会导致悬垂指针。
修复方法:
-
直接返回值(转移所有权):
#![allow(unused)] fn main() { fn returns_owned_string() -> String { let s = String::from("hello"); s // ✅ 移动所有权给调用者 } } -
使用静态字符串:
#![allow(unused)] fn main() { fn returns_static() -> &'static str { "hello" // ✅ 字符串字面量有 'static 生命周期 } } -
使用生命周期标注(高级主题,后续章节详述):
#![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; 试图使用已经移动给 y 的 x。第 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: 确实有性能开销,但:
- 优先使用引用 - 大部分情况不需要克隆
- 只在必要时克隆 - 当确实需要两份独立数据时
- 使用
Rc/Arc- 需要共享所有权时的智能指针
性能敏感的代码可以进行基准测试,但先保证正确性。
Q: 如何调试"值已移动"的错误?
A: 遵循这个流程:
-
编译器会告诉你移动发生在哪里:
value moved here -
问自己:
- 我真的需要所有权吗?还是只需要读取?→ 使用引用
- 我需要两份独立的数据吗?→ 使用
clone() - 可以多线程共享吗?→ 使用
Arc
-
画出所有权流程图:
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:
StringVec<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 会自动处理。
小结
核心要点:
- 所有权是 Rust 管理内存的方式,每个值有且只有一个所有者
- 赋值 = 移动(对于非 Copy 类型),原变量不再可用
- 函数参数默认转移所有权,使用引用避免移动
- 不能返回局部变量的引用,会创建悬垂指针
- 使用
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."
推荐学习流程:
- 先自己写代码,让编译器报错
- 仔细阅读错误信息(Rust 的编译器是最好的老师)
- 如果卡住超过 15 分钟,查看答案
- 关掉答案,从头自己写一遍
下一步:
- 学习 借用和引用 - 如何在不转移所有权的情况下使用值
- 理解 生命周期 - 确保引用不会超出有效范围
术语表
| English | 中文 |
|---|---|
| Ownership | 所有权 |
| Move | 移动 |
| Borrow | 借用 |
| Copy trait | Copy trait |
| Dangling pointer | 悬垂指针 |
| Drop | 丢弃/释放 |
完整示例:src/basic/ownership_sample.rs
延伸阅读
学习完所有权后,你可能还想了解:
- Rust Book 所有权章节 - 官方深入讲解
- 借用检查器原理 - 编译器如何实现
- Cell 和 RefCell - 内部可变性
选择建议:
继续学习
💡 记住:所有权是 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 的创新:
"让编译器在编译时检查内存安全,而不是在运行时"
所有权系统的核心思想:
- 每个值有一个所有者 - 明确责任
- 所有者离开作用域,值被清理 - 自动内存管理
- 借用检查 - 防止悬垂指针
对比其他语言:
| 语言 | 内存管理方式 | 优点 | 缺点 |
|---|---|---|---|
| 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"的错误,本章适合你。生命周期标注听起来复杂,但一旦理解,就能轻松解决这类错误。
你会学到什么
完成本章后,你可以:
- 理解生命周期的概念和作用
- 识别需要生命周期标注的场景
- 使用
'a语法标注生命周期 - 理解生命周期省略规则
- 解决常见的生命周期错误
前置要求
学习本章前,你需要理解:
第一个例子
让我们看一个最简单的生命周期示例:
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 需要生命周期:
| 概念 | Python | Java | C++ | 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 self,self 的生命周期被赋给所有输出生命周期
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 的生命周期被捕获
小结
核心要点:
- 生命周期: 引用的有效作用域,防止悬垂引用
- 标注语法:
&'a Type表示引用活至少'a这么久 - 省略规则: 编译器自动推断常见模式
- 结构体: 有引用字段必须标注生命周期
- '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 - 泛型中的生命周期标注
知识检查
快速测验(答案在下方):
- 这段代码能编译通过吗?
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
-
'static生命周期意味着什么? -
什么时候需要标注生命周期?
点击查看答案与解析
- ❌ 不能 - 需要生命周期标注
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str - 数据在整个程序运行期间都有效(如字符串字面量)
- 当编译器无法自动推断时(多个引用参数且返回引用)
关键理解: 生命周期标注告诉编译器引用的有效范围。
继续学习
💡 记住:生命周期是 Rust 的安全保障。标注生命周期不是负担,而是编译器帮助你避免错误的工具!
结构体
开篇故事
想象你正在设计一个游戏角色。每个角色有名字、生命值、等级、装备等属性。你不会为每个属性创建单独的变量,而是把它们组合在一起形成一个"角色"。Rust 的结构体就是这样的工具箱 - 它把相关的数据打包在一起,让它们作为一个整体被管理。
本章适合谁
如果你已经理解了变量和所有权,现在想学习如何组织复杂的数据,本章适合你。结构体是 Rust 中最常用的数据组织方式,所有 Rust 程序员每天都在使用。
你会学到什么
完成本章后,你可以:
- 定义结构体并创建实例
- 使用字段初始化简写语法
- 实现结构体方法(关联函数)
- 理解所有权在结构体中的工作方式
- 使用结构体更新语法和元组结构体
前置要求
学习本章前,你需要理解:
第一个例子
让我们定义一个简单的矩形结构体:
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_count从user1移动到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 没有 class 和 extends?答案是 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
修复方法:
-
使用引用而不是移动:
let user2 = &user1; // 借用,不移动 println!("{}", user1.username); // ✅ 可以 -
不要使用更新语法,手动复制所有字段:
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: ✅ 通过 -
age是u32,实现了Copytrait
解析:
String 会被移动,但 u32 会复制。所以p1.age仍然可用,但p1.name不可用。
练习 2: 使用更新语法
使用更新语法补全代码,使得 user2 的 email 不同,其他字段和 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.username 和 user1.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: 遵循这个原则:
使用结构体:
- 字段有明确含义(如
name、age) - 需要字段名提高可读性
- 字段可能变化或扩展
使用元组结构体:
- 字段是同类数据(如坐标 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 告诉编译器:引用的有效期至少和结构体一样长。
这是高级主题,后续章节会详细讨论。
小结
核心要点:
- 结构体组合相关数据 - 像数据库记录一样组织信息
- 字段初始化简写 - 当变量名和字段名相同时可以省略
- 更新语法
..instance- 从已有实例创建新实例 - 所有权规则适用 - 字段可以移动、借用、复制
- 元组结构体用于简单包装 - 当只需要组合不需要字段名时
关键术语:
- 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);
}
为什么用结构体:
- 可读性 - 字段名称说明用途
- 类型安全 - 编译器检查字段类型
- 可维护性 - 添加新字段不影响现有代码
- 文档化 - 结构本身就是文档
🧪 动手试试:设计结构体
练习:为图书管理系统设计结构体
// 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 结构体的字段就像这些表格栏目 - 它们定义了结构体可以存储什么类型的数据。
本章适合谁
如果你已经学完了结构体基础,现在想深入了解如何定义、访问和操作结构体的字段,本章适合你。
你会学到什么
完成本章后,你可以:
- 定义结构体字段的类型
- 使用公有 (pub) 和私有字段
- 理解字段所有权的移动规则
- 使用字段初始化简写语法
前置要求
第一个例子
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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 字段定义 | class: self.x | private int x; | int x; | x: i32 | Rust 必须标注类型 |
| 字段访问 | obj.x | obj.getX() / obj.x | obj.x | obj.x 或 pub 字段 | Rust 可控制字段可见性 |
| 字段可见性 | 公开(无控制) | private 默认 | 公开 | 私有默认,需 pub | Rust 默认私有 |
| 字段所有权 | 引用 | 引用 | 可复制或引用 | 移动或 Copy | Rust 有所有权语义 |
| 字段简写 | 无 | 无 | 无 | field: field → field | Rust 有简写语法 |
核心差异: 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: 无法运行时获取,字段在编译时确定。
小结
要点:
- 语法:
字段名:类型 - Copy 类型复制: 非 Copy 类型移动
- 公有/私有:
pub控制访问 - 字段简写: 变量名=字段名可省略
术语:
- Field (字段): 结构体数据成员
- Copy trait: 决定是否复制
- Visibility (可见性): 访问控制
下一步:
术语表
| English | 中文 |
|---|---|
| Field | 字段 |
| Public field | 公有字段 |
| Private field | 私有字段 |
完整示例:src/basic/rectangle.rs
💡 提示:字段是你与结构体交互的主要方式!
知识检查
快速测验(答案在下方):
-
如何初始化结构体时省略字段?
-
结构体更新语法是什么?
-
元组结构体和普通结构体有什么区别?
点击查看答案与解析
- 不能省略 - 所有字段必须初始化(除非有默认值)
Struct { field1: value, ..existing_instance }- 元组结构体有匿名命名字段,普通结构体有命名字段
关键理解: 结构体更新语法可以减少重复代码。
延伸阅读
学习完结构体字段后,你可能还想了解:
选择建议:
继续学习
相关章节:
返回: 基础入门
结构体方法
开篇故事
想象你有了一辆自行车。自行车不仅是一堆零件的组合(车架、轮子),它还能做动作:可以蹬踏板加速、可以刹车减速。Rust 的方法就像是结构体的"动作" - 它让结构体不仅能存储数据,还能对外界做出反应。
本章适合谁
如果你已经学完了结构体和字段,现在想让结构体"动起来"(不仅仅是数据容器),本章适合你。这是迈向面向对象编程的关键一步。
你会学到什么
完成本章后,你可以:
- 为结构体实现方法
- 理解
self、&self、&mut self的区别 - 创建关联函数(类似静态方法)
- 使用方法修改结构体字段
前置要求
学习本章前,你需要理解:
第一个例子
定义一个矩形结构体并添加计算面积的方法:
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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 方法定义 | def method(self): | void method() | void method() | fn method(&self) | Rust 用 impl 块 |
| self 参数 | self 显式 | this 隐式 | this 隐式 | &self 显式 | Rust 显式且可选引用 |
| 可变方法 | 无需声明 | 无需声明 | 无需声明 | 需要 &mut self | Rust 需显式可变借用 |
| 关联函数 | @staticmethod | static method() | static method() | fn new() (无 self) | Rust 无 static 关键字 |
| 方法链 | return self | return this | return *this | &mut Self | Rust 返回引用 |
核心差异: 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) { /* 公有方法 */ }
}
小结
核心要点:
impl块: 为结构体添加方法&self: 借用,最常用&mut self: 可变借用,修改字段self: 获取所有权,消耗实例- 关联函数: 类似静态方法,没有
self
术语:
- Method (方法): 与结构体关联的函数
- Receiver (接收者): 方法的第一个参数(
self) - Associated function (关联函数): 没有
self的方法
下一步:
术语表
| English | 中文 |
|---|---|
| Method | 方法 |
| impl | 实现 |
| Receiver | 接收者 |
| Associated function | 关联函数 |
完整示例:src/basic/rectangle.rs
💡 提示:方法让结构体"活"起来 - 它们不仅能存储数据,还能处理数据!
知识检查
快速测验(答案在下方):
-
方法和函数有什么区别?
-
&self、&mut self、self的区别? -
关联函数和实例方法的区别?
点击查看答案与解析
- 方法在
impl块中定义,第一个参数是self &self借用,&mut self可变借用,self获取所有权- 关联函数没有
self参数(如new()),实例方法有self
关键理解: 方法是附加到结构体上的函数,self 决定访问模式。
延伸阅读
学习完结构体方法后,你可能还想了解:
- 关联函数 - new() 等工厂方法
- 方法链 - Builder 模式
- Deref trait - 智能指针方法调用
选择建议:
继续学习
相关章节:
返回: 基础入门
枚举
开篇故事
想象你在设计一个交通信号系统。信号灯只有三种状态:红、黄、绿。你不会希望有人随意设置成"紫色"或"蓝色"。Rust 的枚举就是一种确保值只能是预定义选项之一的机制 - 它让不可能的状态无法表示。
本章适合谁
如果你已经学完了结构体,想学习如何表示"有限选项"的数据,本章适合你。枚举是 Rust 类型系统的重要组成部分,与模式匹配配合使用非常强大。
你会学到什么
完成本章后,你可以:
- 定义枚举类型
- 使用 match 进行模式匹配
- 理解 Option 和 Result 枚举
- 为枚举实现方法
- 使用带数据的枚举变体
前置要求
第一个例子
定义一个交通灯枚举:
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 强大?
- 类型安全: 编译器保证你处理了所有可能的情况
- 数据携带: 变体可以携带任意类型的数据,不只是整数
- 模式匹配:
match表达式可以解构并提取数据
Rust 的枚举不是简单的整数常量,而是真正的代数数据类型。这让 Option 和 Result 这样的类型成为可能,从根本上避免了空指针错误。
常见错误
错误 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: 显式标记可能为空,编译器强制检查
小结
核心要点:
- 枚举定义有限选项: 值只能是预定义的变体之一
- match 强制穷尽: 必须处理所有情况
- Option 替代 null: 安全的空值处理
- Result 处理错误: 函数式错误处理
- 变体可以带数据: 灵活的类型表示
术语:
- 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("用户不存在"),
}
延伸阅读
学习完枚举后,你可能还想了解:
- Option 类型深入 - 为什么 Rust 没有 null
- 模式匹配高级用法 - 高级模式
- Newtype 模式 - 类型安全封装
选择建议:
知识检查
快速测验(答案在下方):
-
枚举和结构体有什么区别?
-
Option<T>的两个变体是什么? -
如何使用
match处理枚举?
点击查看答案与解析
- 结构体 = 所有字段同时存在,枚举 = 每次只有一个变体
Some(T)和None- 使用
match表达式匹配每个变体并处理
关键理解: 枚举是 Rust 的类型安全替代品,替代其他语言的 null。
继续学习
前一章: 结构体
下一章: 特征 (Traits)
相关章节:
返回: 基础入门
特征 (Traits)
开篇故事
想象你开了一家手机店。店里有 iPhone、Android 手机、老人机。虽然它们不一样,但都能"打电话"和"发短信"。Rust 的特征就像这个"手机"的概念 - 它定义了"能做什么",而不关心具体是什么手机。
本章适合谁
如果你已经学完了结构体和枚举,现在想学习如何定义通用行为,本章适合你。特征是 Rust 泛型和代码复用的核心。
你会学到什么
- 定义和实现特征
- 使用特征作为约束
- 理解特征对象
- 实现标准库特征
- 使用默认方法实现
前置要求
第一个例子
定义一个可以"叫"的特征:
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;
}
小结
要点:
- 特征定义行为: 描述类型能做什么
- 实现提供具体方法:
impl Trait for Type - 默认方法减少重复: 可选覆盖
- 特征约束限制泛型:
T: Trait - 特征对象实现多态:
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?
-
默认方法实现的作用?
点击查看答案与解析
- Rust trait 更灵活,支持默认实现和关联类型
- 孤儿规则:trait 或类型至少有一个在当前 crate 中
- 提供默认行为,实现者可以选择覆盖
关键理解: trait 是 Rust 多态的核心机制。
继续学习
相关章节:
返回: 基础入门
特征对象 (Trait Objects)
开篇故事
想象你在经营一家动物园。你需要一个函数来让所有动物发出声音。如果用泛型,你需要为每种动物(猫、狗、鸟)创建单独的函数版本。但如果有特征对象,你可以创建一个"动物"容器,放入任何实现了 Animal 特征的动物,然后统一调用 make_sound()。这就是特征对象的核心思想:在运行时处理不同类型的值,只要它们实现相同的特征。
特征对象是 Rust 实现运行时多态的方式。它让你可以编写更灵活、可扩展的代码,特别是在需要存储不同类型的集合时。
本章适合谁
如果你已经理解了特征(trait)和泛型,现在想学习如何在运行时处理多种类型,或者需要将不同类型的值存储在同一个集合中,本章适合你。
你会学到什么
完成本章后,你可以:
- 理解特征对象和动态分发的概念
- 使用
dyn 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: 任何实现了Animaltrait 的类型Box: 堆分配,因为特征对象大小在编译时未知Vec<Box<dyn Animal>>: 可以存储不同类型的动物
第 22 行 animal.make_sound() 在运行时决定调用哪个实现(动态分发)。
Python/Java/C++ vs Rust 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | 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) 机制
特征对象在内存中包含:
- 指向实际数据的指针
- 指向虚表(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);
}
对象安全规则:
- 方法不能返回
Self - 方法不能有泛型参数
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); // 泛型 — 编译器不知道要生成多少种版本! } }
对象安全规则:
- 方法不能返回
Self(编译器不知道返回多大) - 方法不能有泛型参数(编译器不知道要生成多少版本)
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);
}
点击查看答案
答案:
- ✅ 可以 - 对象安全
- ❌ 不可以 - 返回
Self - ❌ 不可以 - 有泛型参数
- ❌ 不可以 -
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 特征对象转换
使用 AsRef 或 Into 进行转换:
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),
}
小结
核心要点:
- 特征对象:
dyn Trait实现运行时多态 - 动态分发: 通过虚表查表调用方法
- 对象安全: trait 必须满足规则才能成为特征对象
- Box 需求: 特征对象大小未知,需要指针
- 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 动态分发
知识检查
快速测验(答案在下方):
- 这段代码能编译通过吗?
trait Foo { fn bar(&self) -> Self; }
let obj: Box<dyn Foo>;
-
dyn Trait和泛型有什么区别? -
什么是对象安全?
点击查看答案与解析
- ❌ 不能 - 返回
Self的 trait 不是对象安全的 dyn Trait是运行时动态分发,泛型是编译时单态化- 对象安全 = trait 可以作为特征对象使用(无
Self返回、无泛型方法)
关键理解: 特征对象牺牲性能换取灵活性。
继续学习
💡 记住:特征对象是 Rust 实现运行时多态的工具。优先使用泛型(静态分发),在需要灵活性时使用特征对象(动态分发)!
泛型 (Generics)
开篇故事
想象你有一个模具,可以用它制作不同材质的杯子——玻璃杯、陶瓷杯、塑料杯。模具本身不关心材质,它定义了杯子的形状,材质由使用者决定。Rust 的泛型就是这样——它让你编写与具体类型无关的代码,让编译器在实际使用时生成特定类型的版本。
本章适合谁
如果你已经理解了结构体和特征,现在想编写可复用的通用代码,本章适合你。泛型是高级 Rust 程序员的必备技能。
你会学到什么
完成本章后,你可以:
- 定义泛型函数和结构体
- 理解单态化 (monomorphization) 过程
- 使用特征约束限制泛型类型
- 使用
where子句简化复杂约束 - 区分泛型与特征对象的使用场景
前置要求
学习本章前,你需要理解:
第一个例子
// 泛型函数 - 可以处理任何类型
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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 泛型定义 | 无需声明 | List | template | fn foo | Rust 用 |
| 类型检查 | 运行时检查 | 编译时检查 | 编译时检查 | 编译时检查 | Rust 和 Java/C++ 类似 |
| 泛型实现 | 无泛型 | 类型擦除 | 编译时展开 | 单态化 | Rust 和 C++ 都是零成本 |
| 约束机制 | 无 | T extends Base | 无强制约束 | T: Trait | Rust 用 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- 特征中的类型占位符phantom_data- 编译时类型标记
选择建议:
- 需要类型级别优化 → 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。这是一个空值初始化模式。
小结
核心要点:
- 泛型允许类型参数化 - 编写与具体类型无关的代码
- 单态化产生专用版本 - 编译时替换,运行时零开销
- 特征约束限制可用类型 -
T: Trait语法 - where 子句简化复杂约束 - 多行约束更清晰
- 生命周期与泛型配合 - 确保引用有效性
术语:
- Generic (泛型): 类型或函数的模板
- Type Parameter (类型参数): 占位符类型 T
- Monomorphization (单态化): 编译时特化过程
- Trait Bound (特征约束): 限制泛型类型的约束
下一步:
术语表
| English | 中文 |
|---|---|
| Generic | 泛型 |
| Type Parameter | 类型参数 |
| Monomorphization | 单态化 |
| Trait Bound | 特征约束 |
| Where Clause | where 子句 |
完整示例: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 函数式编程的核心。
你会学到什么
完成本章后,你可以:
- 定义和使用闭包
- 理解闭包捕获环境的方式(by ref, by mut ref, by value)
- 区分 Fn、FnMut、FnOnce trait
- 在迭代器中使用闭包
- 使用
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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 闭包语法 | lambda x: x + 1 | x -> x + 1 | [](int x) { return x+1; } | ` | x |
| 捕获环境 | 自动捕获(引用) | 需声明 final 变量 | 显式 [&] 或 [=] | 自动推断借用/移动 | Rust 编译器自动选择 |
| 类型标注 | 不需要 | 需明确类型 | 可选 | 可选或推断 | Rust 第一次调用后固定 |
| 修改环境 | nonlocal x | 不支持 | [&] 可修改 | 需要 mut 和 FnMut | Rust 用 trait 区分 |
| 存储闭包 | 直接赋值 | 需要接口类型 | std::function | Box<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; // 不捕获环境的闭包
延伸阅读
学习完本章,你可能还想了解:
Fn trait- 标准库 Fn traitBox<dyn Fn>- 特征对象rayon- 并行迭代器库
选择建议:
- 学习标准 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 可访问。
小结
核心要点:
- 闭包是匿名函数 - 简洁语法,类型推断,可存储在变量中
- 捕获环境变量 - 自动选择借用或移动,实现"记忆"功能
- 三种 trait 层次 -
Fn(只读) <FnMut(修改) <FnOnce(消耗) - 作为参数传递 - 泛型 + trait 约束,传递"行为"
- move 关键字 - 强制所有权转移,用于线程、返回等场景
- 类型推断机制 - 第一次调用时固定类型参数
源码示例对照:
| 源码位置 | 概念 | 示例 |
|---|---|---|
closure_sample.rs:4 | 基本闭包 | `let add_one = |
closure_sample.rs:24 | 捕获环境 | `let add_captured = |
closure_sample.rs:58 | FnMut | `let mut increment = |
closure_sample.rs:80 | FnOnce | `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 关键字): 强制所有权转移
下一步:
术语表
| English | 中文 | 说明 |
|---|---|---|
| Closure | 闭包 | 可捕获环境的匿名函数 |
| Capture Environment | 捕获环境 | 闭包访问外部变量的机制 |
| Fn Trait | Fn 特征 | 只读捕获,可多次调用 |
| FnMut Trait | FnMut 特征 | 可变捕获,可修改环境 |
| FnOnce Trait | FnOnce 特征 | 消耗所有权,仅一次调用 |
| Move Keyword | move 关键字 | 强制所有权转移 |
| Anonymous Function | 匿名函数 | 无名称的函数定义 |
| Type Inference | 类型推断 | 编译器自动确定类型 |
| Environment | 环境 | 闭包定义时的作用域 |
项目实例
完整示例位于: src/basic/closure_sample.rs
代码示例覆盖:
- 基本闭包定义 (第 4-7 行)
- 闭包作为函数参数 (第 11-20 行)
- 捕获环境变量 (第 23-37 行)
- 返回不同类型 (第 40-51 行)
- FnMut 使用 (第 54-76 行)
- FnOnce 和 move (第 79-90 行)
运行示例:
# 在项目根目录执行
cargo run
# 输出包含所有闭包示例结果
💡 提示: 闭包是 Rust 函数式编程的核心 - 把"行为"当作"数据"传递,让代码更具表达力!
继续学习
相关章节:
返回: 基础入门
模块系统
开篇故事
想象你在经营一家大型超市。如果所有商品(食品、日用品、电器)都堆在一个大房间里,顾客会疯掉,员工也找不到东西。你需要把商品分类放在不同的区域:食品区、日用品区、电器区。每个区域有自己的入口,有些区域对所有顾客开放,有些区域(如仓库)只允许员工进入。
Rust 的模块系统就是超市的分区系统——它帮你组织代码,控制访问权限,让大型项目保持清晰和可维护。
本章适合谁
如果你正在编写超过 500 行的 Rust 项目,或者想学习如何组织多文件项目,本章适合你。
你会学到什么
完成本章后,你可以:
- 使用
mod创建模块层次结构 - 使用
pub控制可见性 - 使用
use简化路径 - 组织多文件项目结构
- 理解
pub(crate)和pub(super) - 避免循环依赖
前置要求
第一个例子
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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 模块定义 | 文件即模块 | package + 文件夹 | namespace | mod 关键字 | Rust 需显式声明 mod |
| 可见性 | 无强制控制 | public/private | 无强制控制 | pub + 默认私有 | Rust 默认私有 |
| 导入语法 | import module | import pkg.Class | using namespace | use crate::module | Rust 用 use + 路径 |
| 嵌套模块 | import pkg.sub | package pkg.sub | namespace ns::sub | mod 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
小结
要点:
- mod 定义模块: 组织代码结构
- pub 控制可见: 默认私有
- use 导入名称: 简化路径
- 路径分层:
crate::module::item - 文件分离: 大模块放独立文件
术语表
| English | 中文 |
|---|---|
| Module | 模块 |
| Visibility | 可见性 |
| Path | 路径 |
| Crate | 包 |
| Re-export | 重新导出 |
| Prelude | 预导入模块 |
完整示例:src/basic/module_sample.rs
延伸阅读
学习完模块系统后,你可能还想了解:
选择建议:
知识检查
快速测验(答案在下方):
-
mod和use的区别是什么? -
如何使模块公开?
-
pub(crate)的作用是什么?
点击查看答案与解析
mod定义模块,use引入名称到当前作用域- 在模块前加
pub:pub mod my_module - 只在当前 crate 内可见,对外部 crate 私有
关键理解: 模块系统帮助组织大型项目和控制可见性。
继续学习
💡 提示:好的模块结构让代码像好文章一样易读!
线程与并发
开篇故事
想象你在经营一家餐厅。如果只有一个厨师(单线程),他必须按顺序完成每道菜:切菜 → 炒菜 → 装盘 → 下一道。这很慢,但不会出错。
如果你雇了多个厨师(多线程),他们可以同时做菜——但问题来了:如果两个厨师都想用同一把刀怎么办?如果一个厨师还没切完菜,另一个就拿走了怎么办?这就是并发编程的核心挑战:协调共享资源的访问。
Rust 的线程系统就像一位经验丰富的餐厅经理——它在编译时就确保不会出现"抢刀"的情况。
本章适合谁
如果你想编写多线程程序提高性能,或者理解 Rust 如何防止数据竞争,本章适合你。
你会学到什么
完成本章后,你可以:
- 使用
thread::spawn创建线程 - 使用
join()等待线程完成 - 使用
move闭包转移所有权到线程 - 使用通道(channel)在线程间传递消息
- 使用
Arc<Mutex<T>>安全共享可变状态 - 理解 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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 线程创建 | threading.Thread() | new Thread() | std::thread | `thread::spawn( | |
| 数据共享 | 随意共享(无检查) | 随意共享(需同步) | 随意共享(需同步) | 编译时检查所有权 | Rust 编译时防止数据竞争 |
| 锁机制 | threading.Lock() | synchronized | std::mutex | Mutex<T> + Arc | Rust 锁包裹数据 |
| 消息传递 | Queue | 需要库 | 需要库 | mpsc::channel | Rust 原生支持通道 |
| 线程安全 | 运行时错误 | 运行时错误 | 运行时错误 | 编译时保证 | 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: 允许多个读线程或一个写线程
小结
要点:
- thread::spawn: 创建新线程
- join(): 等待线程完成
- 消息传递: 线程间安全通信
- 避免共享可变状态: 使用 Mutex 或通道
安全规则:
- ❌ 不要用
static mut - ❌ 不要在线程间共享可变引用
- ✅ 使用
Arc<Mutex<T>>安全共享 - ✅ 使用通道 (channel) 传递消息
术语表
| English | 中文 |
|---|---|
| Thread | 线程 |
| Data race | 数据竞争 |
| Mutex | 互斥锁 |
| Channel | 通道 |
| Deadlock | 死锁 |
| Send/Sync | 线程安全 trait |
完整示例:src/basic/threads_sample.rs
知识检查
快速测验(答案在下方):
-
Rc<T>可以在多线程中使用吗? -
这段代码有什么问题?
let data = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("{:?}", data);
});
- 通道 (channel) 和 Mutex 有什么区别?
点击查看答案与解析
- ❌ 不能 -
Rc不是线程安全的,应该使用Arc data的所有权没有转移到闭包,需要使用move- 通道 = 消息传递(所有权转移),Mutex = 共享状态(借用)
关键理解: Rust 在编译时防止数据竞争。
继续学习
🔴 警告:并发编程容易出错。始终使用高级抽象(Arc、Mutex、通道),避免原始线程操作!
条件编译
开篇故事
想象你在组装一台电脑。不同地区需要不同的电源插头:美国用 110V 两脚插头,欧洲用 220V 圆脚插头。你不会为每个地区生产不同型号的电脑,而是设计一个通用主板,根据目标地区安装不同的电源模块。条件编译就是 Rust 的"电源适配器"——同一份代码,根据不同平台编译出不同的程序。
本章适合谁
如果你需要编写跨平台代码(Windows/macOS/Linux),或者想实现可选功能(如日志、调试模式),本章适合你。条件编译是系统级编程的必备技能。
你会学到什么
完成本章后,你可以:
- 使用
#[cfg]属性控制编译 - 编写平台特定代码
- 使用
cfg_if宏简化条件编译 - 定义和使用特性标志(feature flags)
- 理解编译时 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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | 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_arch | CPU 架构 | "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]属性 - 更清晰的分支结构
- 支持
else和compile_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=mir 或 cargo 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 = "\\";
小结
核心要点:
- cfg 属性:
#[cfg(...)]控制编译 - 特性标志: 可选功能,零开销
- 平台代码:
target_os,target_arch - 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
知识检查
快速测验(答案在下方):
-
#[cfg]和#[cfg_attr]有什么区别? -
如何在编译时定义自定义特性标志?
-
cfg_if!宏相比多个#[cfg]有什么优势?
点击查看答案与解析
#[cfg]条件编译代码,#[cfg_attr]条件添加属性cargo rustc --cfg my_feature或在 Cargo.toml 的[features]中定义cfg_if!更清晰,支持else分支,避免重复#[cfg]
关键理解: 条件编译是编译时决定,零运行时开销。
继续学习
💡 记住:条件编译让你的代码跨平台运行,但要保持函数签名一致!
指针与不安全代码
🔴 高危警告
本章涉及 Rust 的 unsafe 特性。这些内容仅用于理解 Rust 的底层机制。除非绝对必要且有充分理由,否则不要在生产线代码中使用 unsafe。
开篇故事
想象你在驾驶一辆汽车。安全模式就像有安全气囊、ABS 刹车辅助、车道偏离警告——系统会保护你不犯错。不安全代码就像关闭所有安全系统,直接操控引擎——你能获得极致性能,但一次失误就可能车毁人亡。
Rust 的 unsafe 就是那个"关闭安全系统"的开关。它不是"邪恶"的,而是强大但危险的工具。本章教你理解它、尊重它、必要时安全地使用它。
本章适合谁
如果你想理解 Rust 内存安全的底层机制,或者需要与 C 代码交互、实现高性能数据结构,本章适合你。
你会学到什么
完成本章后,你可以:
- 理解原始指针语法和创建方式
- 掌握 unsafe 块的 5 种操作
- 识别何时必须使用 unsafe
- 使用安全抽象封装 unsafe 代码
- 理解未定义行为(UB)的危害
前置要求
第一个例子
fn main() {
let mut num = 5;
// ✅ 安全引用
let r1 = #
let r2 = #
// ⚠️ 原始指针(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
合法场景:
- FFI(外部函数接口):
// 调用 C 库
extern "C" {
fn printf(format: *const i8, ...) -> i32;
}
unsafe {
printf(b"Hello from C!\0".as_ptr() as *const i8);
}
- 高性能数据结构:
// 实现 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;
}
}
- 硬件操作:
// 内存映射 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:
- 使用 Miri 工具检测未定义行为:
cargo +nightly miri run - 启用 AddressSanitizer:
RUSTFLAGS="-Z sanitizer=address" cargo run - 编写充分的单元测试
- 使用
#[deny(unsafe_op_in_unsafe_fn)]强制显式 unsafe
Q: 标准库中有多少 unsafe 代码?
A: 约 10-15%。像 Vec、String、HashMap 这样的核心数据结构底层都使用 unsafe,但它们提供了安全的公共接口。
Q: 如何安全地实现自定义集合?
A: 遵循以下模式:
- 使用
MaybeUninit管理未初始化内存 - 在
Drop中正确释放资源 - 提供安全的公共接口
- 编写充分的测试(包括边界情况)
- 使用 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;
}
}
小结
核心原则:
- unsafe 不是"随便用": 只在必要且可控时使用
- 封装 unsafe: 提供安全的接口
- 记录安全契约: 为什么 unsafe 是安全的
- 优先安全抽象: Rust 标准库已经提供了大部分需要的工具
关键术语:
- Raw Pointer: 原始指针
- Unsafe Block: unsafe 块
- Undefined Behavior (UB): 未定义行为
- FFI: 外部函数接口
- Safety Invariant: 安全不变量
术语表
| English | 中文 |
|---|---|
| Raw pointer | 原始指针 |
| Unsafe block | unsafe 块 |
| Undefined behavior | 未定义行为 |
| Safety contract | 安全契约 |
| FFI | 外部函数接口 |
| Memory safety | 内存安全 |
完整示例:src/basic/pointer_sample.rs
知识检查
快速测验(答案在下方):
-
原始指针和引用有什么区别?
-
什么时候需要使用
unsafe? -
*const T和*mut T的区别?
点击查看答案与解析
- 原始指针不遵循借用规则,可以为空或悬垂
- 解引用原始指针、调用 unsafe 函数、访问可变静态
*const T不可变,*mut T可变
关键理解: unsafe 是强大但危险的工具,应谨慎使用并封装在安全接口中。
延伸阅读
学习完指针与不安全代码后,你可能还想了解:
- Rustonomicon - 不安全 Rust 指南
- FFI 指南 - 与 C 代码交互
- 智能指针深入 - Box, Rc, Arc
选择建议:
- 想学习日志 → 继续学习 日志记录
- 想学习追踪 → 跳到 追踪 (Tracing)
继续学习
- 下一步:日志记录
- 进阶:Unsafe Rust
- 回顾:所有权
🔴 记住:unsafe 让你对编译器说"我知道我在做什么,相信我"。确保你真的知道!
日志记录 (Logger)
开篇故事
想象你在驾驶一辆汽车,仪表盘告诉你车速、油量、发动机状态。没有这些信息,你就像在盲开。Rust 程序的日志就是仪表盘 - 它告诉你程序正在发生什么,帮助你诊断问题。
本章适合谁
如果你已经能写基础 Rust 代码,现在想知道如何让程序"开口说话"(输出运行信息),本章适合你。日志是调试和监控的关键工具。
你会学到什么
- 使用 env_logger 配置日志
- 不同日志级别(info, debug, error, trace)
- 自定义日志格式
- 日志与随机数生成
- 实际应用中的日志模式
前置要求
学习本章前,你需要理解:
第一个例子
// 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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 日志框架 | logging 模块 | Log4j / SLF4J | 无标准 | log crate + 实现 | Rust 分离接口和实现 |
| 日志级别 | DEBUG/INFO/WARN/ERROR | 同样 | 无标准 | trace/debug/info/warn/error | Rust 多了 trace 级别 |
| 配置方式 | logging.basicConfig() | 配置文件 | 无标准 | 环境变量 RUST_LOG | Rust 用环境变量控制 |
| 初始化 | 自动 | 需配置 | 手动 | 必须调用 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: 检查三点:
- 是否调用了
env_logger::init() RUST_LOG环境变量设置- 日志级别是否正确
# 临时设置
RUST_LOG=debug cargo run
Q: 如何把日志输出到文件?
A: 使用 env_logger 的 write_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 - 生产:
warn或error
小结
核心要点:
- 5 个日志级别: trace → debug → info → warn → error
- 必须初始化:
env_logger::init()或Builder::new().init() - 环境变量控制:
RUST_LOG=debug cargo run - 灵活配置: 可为不同模块设置不同级别
- 性能考虑: 生产环境使用较高日志级别(减少输出)
术语:
- Log level (日志级别): 日志的严重等级
- Filter (过滤): 根据级别筛选日志
- Formatter (格式化器): 日志输出格式
下一步:
- 继续:追踪 (Tracking) - 异步日志
- 相关:错误处理
术语表
| English | 中文 |
|---|---|
| Logger | 日志器 |
| Log level | 日志级别 |
| Filter | 过滤器 |
| Format | 格式化 |
完整源码:src/basic/logger_sample.rs
💡 提示:好的日志就像飞机的黑匣子 - 平时看不见,出问题时能救命!
知识检查
快速测验(答案在下方):
-
logcrate 和env_logger的关系是什么? -
日志级别有哪些?
-
如何设置日志级别?
点击查看答案与解析
log提供 API,env_logger是具体实现- error, warn, info, debug, trace
- 设置
RUST_LOG环境变量
关键理解: 日志是调试和监控生产应用的重要工具。
延伸阅读
学习完日志记录后,你可能还想了解:
- tracing 框架 - 更强大的结构化日志
- env_logger 配置 - 环境变量控制日志
- slog crate - 结构化日志库
选择建议:
- 想学习结构化日志 → 继续学习 追踪 (Tracing)
- 想学习可见性 → 跳到 可见性控制
继续学习
前一章: 指针与不安全代码
下一章: 追踪 (Tracing)
相关章节:
- 追踪 (Tracing) - 高级日志
- 模块系统 - 日志模块
返回: 基础入门
追踪 (Tracing)
开篇故事
想象你在玩一个复杂的桌游,每走一步都有人记录:"玩家 A 从起点移动到第 5 格"。如果游戏出错了,你可以回放整个游戏过程找出问题。Rust 的追踪 (tracing) 就是这样 - 它记录程序的每一步执行,帮助你理解异步代码的执行流程。
本章适合谁
如果你已经学习了基础日志,现在想深入理解异步程序的执行流程,本章适合你。追踪是现代 Rust 异步编程的必备工具。
你会学到什么
- tracing 与 log 的区别
- Span的概念和使用
- 异步函数追踪
- 自定义追踪事件
- 性能影响分析
前置要求
- 日志记录 - 基础日志概念
- 异步编程 - 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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 追踪框架 | 无标准 | 无标准 | 无标准 | tracing crate | Rust 有专用追踪框架 |
| 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% 开销
- 生产建议: 使用
warn或error级别
Q: 如何输出到文件?
A:
use tracing_subscriber::fmt;
use std::fs::File;
let file = File::create("app.log").unwrap();
fmt::fmt()
.with_writer(file)
.init();
小结
核心要点:
- Span vs Event: Span 有时间跨度,Event 是时间点
- 自动仪器化:
#[tracing::instrument]减少样板代码 - 异步友好: span 可以跟随任务跨线程
- 性能影响: 生产环境使用较高级别
- 调试利器: 可视化程序执行流程
术语:
- Span: 表示一段时间的追踪
- Event: 时间点的事件
- Instrument: 自动添加追踪
- Subscriber: 处理追踪输出的组件
下一步:
- 相关:日志记录
- 进阶:异步调试
术语表
| English | 中文 |
|---|---|
| Span | 跨度 |
| Event | 事件 |
| Instrument | 仪器化 |
| Subscriber | 订阅器 |
完整源码:src/basic/tracing_sample.rs
💡 提示:追踪让你像看慢动作回放一样理解异步代码!
知识检查
快速测验(答案在下方):
-
tracing和log的区别是什么? -
span 的作用是什么?
-
如何添加自定义字段到 span?
点击查看答案与解析
tracing支持结构化日志和 span,log是简单文本- span 表示一段时间内的操作,可嵌套
- 在
#[instrument]中添加参数或使用Span::current().record()
关键理解: tracing 是现代 Rust 应用的首选日志框架。
延伸阅读
学习完追踪后,你可能还想了解:
- tracing-subscriber - 日志订阅器配置
- OpenTelemetry - 分布式追踪
- tracing-tree - 树形日志输出
选择建议:
继续学习
相关章节:
返回: 基础入门
可见性 (Visibility)
开篇故事
想象你在一个公司工作。有些信息是公开的(公司公告板),有些是部门内部的(部门会议),有些是私密的(HR 档案)。Rust 的可见性控制就像公司的信息分级 - 它决定哪些代码和数据可以被谁访问。
本章适合谁
如果你已经学完了结构体和模块,现在想学习如何控制代码的访问权限,本章适合你。可见性是封装和信息隐藏的基础。
你会学到什么
- pub 关键字的使用
- 结构体字段可见性
- 模块间可见性
- 私有构造器模式
- 封装最佳实践
前置要求
第一个例子
// 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 对比
如果你有其他语言经验,这个对比会帮助你快速理解:
| 概念 | Python | Java | C++ | Rust | 关键差异 |
|---|---|---|---|---|---|
| 默认可见性 | 公开 | 包内可见 | 公开 | 私有 | Rust 默认私有 |
| 公开关键字 | 无需声明 | public | 无需声明 | pub | Rust 需显式公开 |
| 包级可见 | 无 | protected | 无 | pub(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)
小结
核心要点:
- 默认私有: 模块、函数、字段默认都是私有的
- pub 控制: 使用
pub关键字公开 - 封装优先: 默认私有,需要时再公开
- 私有构造器: 通过方法控制对象创建
- 信息隐藏: 隐藏实现细节,暴露稳定接口
术语:
- Visibility (可见性): 代码的访问权限
- Encapsulation (封装): 隐藏实现细节
- Public interface (公有接口): 对外暴露的方法
- Private implementation (私有实现): 内部实现细节
下一步:
- 相关:模块
- 进阶:封装模式
术语表
| English | 中文 |
|---|---|
| Visibility | 可见性 |
| Encapsulation | 封装 |
| Public | 公有 |
| Private | 私有 |
完整源码:src/basic/visiable_sample.rs
💡 提示:好的可见性设计让你的代码像精密的钟表 - 用户只需要看表盘,不需要知道齿轮怎么转!
知识检查
快速测验(答案在下方):
-
Rust 中默认的可见性是什么?
-
pub、pub(crate)、pub(super)的区别? -
如何重新导出名称?
点击查看答案与解析
- 默认私有(private)
pub公开,pub(crate)crate 内可见,pub(super)父模块可见- 使用
pub use:pub use inner_module::MyType
关键理解: 可见性控制是封装和 API 设计的重要部分。
延伸阅读
学习完可见性控制后,你可能还想了解:
- pub use 模式 - 公共 API 设计
- crate 可见性 - 包级别可见性
- API 设计指南 - Rust API 规范
选择建议:
继续学习
前一章: 追踪 (Tracing)
下一章: 高级进阶 🎓
相关章节:
返回: 基础入门
阶段复习:基础部分
开篇故事
想象你刚学完驾驶理论——你知道交通规则、标志含义、操作步骤。但真正上路前,你需要一次综合练习:在模拟环境中把所有知识串联起来。阶段复习就是你的"驾驶模拟考"——把分散的概念整合成完整的能力。
本章适合谁
如果你已经完成了基础部分(第 1-10 章),现在想检验自己的学习成果,本章适合你。
你会学到什么
完成本章后,你可以:
- 综合运用所有权、借用、生命周期知识
- 识别和修复常见的 Rust 编译错误
- 设计包含结构体、枚举、特征的系统
- 理解模块可见性和代码组织
前置要求
完成以下章节:
第一个例子
回顾所有权的核心模式:
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 的所有权已移动给 s2,s1 不再有效。
修复方法:
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(),即使 hello 是 pub 的。
修复方法:
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 到模块声明 |
小结
核心要点:
- 所有权是 Rust 内存安全的核心
- 借用规则防止数据竞争
- 特征实现多态
- 模块系统组织代码
- 复习是巩固知识的关键
关键术语:
- 所有权 (Ownership): 对值的独占访问
- 借用 (Borrowing): 临时访问,不转移所有权
- 特征 (Trait): 接口定义
- 模块 (Module): 代码组织单元
术语表
| English | 中文 |
|---|---|
| Ownership | 所有权 |
| Borrowing | 借用 |
| Trait | 特征 |
| Module | 模块 |
| Visibility | 可见性 |
| Closure | 闭包 |
| Generic | 泛型 |
继续学习
💡 记住:复习是学习的重要部分。不要急于前进,确保每个概念都理解了!
高级进阶 (Advance)
开篇故事
想象你已经学会了 Rust 的基础:变量、所有权、结构体、枚举。现在你想建造一座真正的房子——不是玩具模型,而是能住人的。这就需要更强大的工具:电钻、锯子、水平仪。Rust 的高级特性就是你的"电动工具"——它们让复杂任务变得简单,让高性能代码成为可能。
本部分涵盖 Rust 生态系统的核心工具:异步编程、数据库操作、Web 开发、数据处理、系统编程、测试与模拟、宏编程。掌握这些,你就能构建生产级应用。
本章适合谁
如果你已经完成了 基础入门,现在想学习 Rust 在实际项目中的应用,本部分适合你。
你会学到什么
完成高级进阶后,你可以:
- 异步编程 - 使用 Tokio 编写高并发网络服务
- 数据库操作 - 使用 SQLx 和 Diesel 操作数据库
- Web 开发 - 使用 Axum 和 Hyper 构建 REST API
- 数据处理 - 序列化/反序列化 JSON、CSV 等格式
- 系统编程 - 文件操作、内存映射、进程管理
- 测试与模拟 - 编写单元测试和集成测试
- 宏编程 - 使用声明宏和过程宏减少代码重复
前置要求
- ✅ 基础入门 全部章节
- ✅ 理解所有权和借用
- ✅ 理解结构体和特征
- ✅ 基本的 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 高级编程的基石。
本章适合谁
如果你已经理解了所有权的概念,现在想处理更复杂的场景(如多所有权、图结构、并发共享),本章适合你。
你会学到什么
完成本章后,你可以:
- 理解智能指针与普通引用的区别
- 使用
Box<T>进行堆分配和定义递归类型 - 使用
Rc<T>实现多所有权 - 使用
RefCell<T>实现内部可变性 - 使用
Arc<T>在线程间安全共享数据 - 识别并解决引用循环导致的内存泄漏
前置要求
第一个例子
最简单的智能指针 Box<T>:
fn main() { // 将 i32 分配到堆上 let b = Box::new(5); println!("b = {}", b); // b 离开作用域时,堆内存被释放 }
发生了什么?
Box::new(5)在堆上分配内存存储5。b拥有这块堆内存的所有权。- 当
b离开作用域,Box的drop方法被调用,释放堆内存。
原理解析
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!("数据已被清理"); } }
小结
核心要点:
- 智能指针: 拥有数据并提供额外元数据或能力。
- Box
: 堆分配,递归类型。 - Rc
: 单线程多所有权。 - RefCell
: 运行时借用检查(内部可变性)。 - Arc
: 多线程多所有权。 - 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 的基础语法和所有权,现在想学习如何在实际项目中优雅地处理错误——特别是异步环境下的错误传播和转换,本章适合你。
你会学到什么
完成本章后,你可以:
- 使用
Result<T, E>和?操作符进行错误传播 - 实现
Fromtrait 自动转换错误类型 - 使用
Box<dyn Error>简化错误处理 - 在异步函数中正确处理错误
- 避免
.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)
}
优点:
- 无需定义错误枚举
- 任何实现
Errortrait 的类型都可以返回
缺点:
- 运行时动态分发(轻微性能开销)
- 调用者无法精确匹配错误类型
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: 三种方案:
- 定义错误枚举 + 实现
From(推荐用于库) - 使用
Box<dyn Error>(推荐用于应用) - 使用
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)
}
小结
核心要点:
- Result<T, E>: Rust 的错误处理类型,显式处理成功和失败
- ? 操作符: 简洁的错误传播语法
- From trait: 自动转换错误类型
- Box
: 简化多种错误的返回 - 避免 .unwrap(): 生产代码使用优雅的错误处理
关键术语:
- panic: 不可恢复的错误,程序终止
- Result: 可恢复错误的枚举类型
- ? operator: 错误传播操作符
- From trait: 类型转换 trait
- Box
: 动态错误类型
下一步:
- 学习 自定义错误类型
- 实践 异步错误处理最佳实践
- 回顾 Result 组合子
术语表
| English | 中文 |
|---|---|
| Error Handling | 错误处理 |
| Panic | 恐慌 |
| Result | 结果 |
| Propagate | 传播 |
| Unwrap | 解包 |
| From Trait | From 特征 |
| Dynamic Dispatch | 动态分发 |
| Type Erasure | 类型擦除 |
完整示例:src/advance/tokio_sample.rs - 异步错误处理模式
相关示例:src/advance/sqlx_sample.rs - 数据库错误处理
知识检查
快速测验(答案在下方):
-
Result<T, E>和Option<T>有什么区别? -
?操作符做了什么? -
Box<dyn Error>的优缺点是什么?
点击查看答案与解析
Result携带错误信息,Option只有有/无- 传播错误:如果是
Err则返回,否则解包 - 优点 = 简单,缺点 = 动态分发、无法匹配具体错误类型
关键理解: 好的错误处理让程序更健壮。
继续学习
- 下一步:数据库操作 - 实际项目中的错误处理
- 进阶:Tokio 异步运行时 - 异步错误处理模式
- 回顾:异步编程基础 - Result 在异步中的使用
💡 记住:好的错误处理让程序更健壮。永远不要忽略错误,显式处理每一个失败场景!
高级特征 (Advanced Traits)
开篇故事
想象你在设计一个通用的遥控器。基础遥控器只能开关电视。高级遥控器不仅能开关,还能根据电视型号自动调整频道,甚至能学习你的习惯。Rust 的特征系统也是如此——除了基础的方法定义,它还支持关联类型、默认泛型参数等高级功能,让你设计出更灵活、更强大的接口。
本章适合谁
如果你已经掌握了特征的基础用法,现在想深入理解标准库中的复杂特征(如 Iterator、Add),或者想解决同名方法冲突,本章适合你。
你会学到什么
完成本章后,你可以:
- 使用关联类型 (Associated Types) 定义特征
- 使用默认泛型类型参数
- 使用完全限定语法 (Fully Qualified Syntax) 解决冲突
- 理解 Supertraits (特征继承)
- 使用 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 方法,同时也实现了 Pilot 和 Wizard 的 fly 方法。编译器默认调用类型自身的方法。要调用特征的,必须用 <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] } } }
小结
核心要点:
- 关联类型: 简化特征实现,确保唯一性。
- 默认泛型参数: 用于运算符重载等场景。
- 完全限定语法: 解决同名方法冲突。
- Supertraits: 特征之间的依赖关系。
- 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 标准库中最强大的工具之一。
你会学到什么
完成本章后,你可以:
- 理解迭代器的惰性求值 (Lazy Evaluation) 特性
- 使用
Iteratortrait 处理集合 - 使用适配器 (Adapters):
map,filter,enumerate - 使用消费者 (Consumers):
collect,fold,sum - 创建自定义迭代器
前置要求
第一个例子
使用迭代器处理向量:
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) } }
小结
核心要点:
- 惰性求值: 迭代器在调用消费者前不执行。
- 适配器:
map,filter,enumerate等转换迭代器。 - 消费者:
collect,fold,sum等消耗迭代器产生结果。 - 自定义迭代器: 实现
Iteratortrait。
关键术语:
- 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 的基础所有权和生命周期,现在想学习如何编写高效的异步程序——比如同时处理多个网络请求、读写文件而不阻塞线程,本章适合你。
你会学到什么
完成本章后,你可以:
- 解释什么是
Future以及它如何工作 - 使用
async/await语法编写异步函数 - 理解
poll机制和执行器 (Executor) 的角色 - 使用组合器 (Combinators) 链接异步操作
- 区分
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_song 和 dance 同时执行?
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:
| 特性 | .await | block_on |
|---|---|---|
| 是否阻塞线程 | 否,只阻塞当前任务 | 是,阻塞整个线程 |
| 使用场景 | async 函数内部 | 同步代码中启动异步 |
| 能否并发 | 能,让出线程给其他任务 | 不能,独占线程 |
| 示例 | let x = foo().await; | block_on(foo()) |
最佳实践:
- 在 async 函数内部总是用
.await - 在
main函数或测试中用block_on进入异步世界
Q: 如何调试异步代码?
A:
-
添加日志追踪:
async fn my_function() { println!("Starting my_function"); let result = some_async_op().await; println!("Got result: {:?}", result); } -
使用 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,但理解它有助于调试复杂异步代码。
小结
核心要点:
- Future 是惰性的 - 创建时不会执行,需要被 poll
- async fn 返回 Future - 需要
.await或block_on来执行 - await 不阻塞线程 - 只阻塞当前任务,让出线程执行其他任务
- async vs async move - 前者按引用捕获,后者按值 move 捕获
- 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
继续学习
- 下一步:Tokio 异步运行时
- 进阶:并发与并行
- 相关:线程与并发
💡 记住:异步编程的核心是"等待时不浪费资源"。当你需要等待 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
解析:
let f1 = task1()只是创建 Future,不会打印 "1"let f2 = task2()只是创建 Future,不会打印 "2"- 立即打印 "start"
block_on(f1)执行 Future,打印 "1"- 打印 "middle"
block_on(f2)执行 Future,打印 "2"- 打印 "end"
关键点:Future 是惰性的,创建时不执行!
问题 2 🟡 (并发执行)
如何让 task1 和 task2 并发执行,并等待两者都完成?
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 生态的事实标准异步运行时。
你会学到什么
完成本章后,你可以:
- 理解 Tokio 运行时的核心组件
- 使用
#[tokio::main]启动异步程序 - 使用
tokio::spawn创建异步任务 - 使用
mpsc通道在任务间传递消息 - 使用
RwLock安全共享状态 - 使用
spawn_blocking运行阻塞代码 - 理解 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 个工作线程
}
小结
核心要点:
- #[tokio::main]: 启动异步运行时
- tokio::spawn: 创建异步任务
- mpsc: 多发送端通道
- RwLock: 读写锁共享状态
- spawn_blocking: 运行阻塞代码
关键术语:
- Runtime: 运行时
- Executor: 执行器
- Reactor: I/O 反应器
- Work-stealing: 工作窃取
- Channel: 通道
术语表
| English | 中文 |
|---|---|
| Runtime | 运行时 |
| Async Task | 异步任务 |
| Channel | 通道 |
| Work-stealing | 工作窃取 |
| Blocking | 阻塞 |
| Spawn | 生成任务 |
完整示例:src/advance/tokio_sample.rs
知识检查
快速测验(答案在下方):
-
mpsc通道的m、p、s、c分别代表什么? -
为什么发送端需要
drop(tx)? -
RwLock和Mutex如何选择?
点击查看答案与解析
- multiple producer, single consumer
- 不 drop 的话接收端会永远等待(认为还有更多消息)
- 读多写少用
RwLock,读写均衡用Mutex
关键理解: Tokio 的通道是异步的,与 std::sync::mpsc 不同。
继续学习
- 下一步:Futures 异步编程
- 进阶:数据库操作
- 回顾:异步编程
💡 记住:Tokio 是 Rust 异步编程的基石。掌握它,你就能构建高并发服务!
Futures 异步编程
开篇故事
想象你在餐厅点餐。传统方式是:点餐 → 等待 → 取餐 → 吃。异步方式是:点餐 → 拿到号牌 → 继续做其他事 → 号牌响了去取餐。Futures 就像这个号牌——它代表一个将来会完成的任务。
本章适合谁
如果你已经了解了 async/await 基础,现在想深入理解 Future trait 和异步组合子,本章适合你。Futures 是 Rust 异步编程的核心抽象。
你会学到什么
完成本章后,你可以:
- 理解 Future trait 的工作原理
- 使用 block_on 执行 Future
- 链式组合多个 Future
- 并发执行多个任务
- 使用 join! 宏并发等待
前置要求
- Tokio 异步运行时 - async/await 基础
- 闭包 - 闭包语法
- 特征 - trait 基础
依赖安装
运行以下命令安装所需依赖:
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)
}
}
小结
核心要点:
- Future trait: 表示异步计算的 trait
- block_on: 在同步上下文执行 Future
- 链式组合: then, map, then 等方法
- 并发执行: join, select 组合多个 Future
- 错误处理: 使用 Result 和错误转换
关键术语:
- Future: 未来值/异步任务
- Poll: 轮询
- Executor: 执行器
- Combinator: 组合子
- block_on: 阻塞执行
- await: 异步等待
术语表
| English | 中文 |
|---|---|
| Future | 未来值/异步任务 |
| Poll | 轮询 |
| Executor | 执行器 |
| Combinator | 组合子 |
| block_on | 阻塞执行 |
| await | 异步等待 |
| join | 并发等待 |
| select | 选择第一个完成 |
知识检查
快速测验(答案在下方):
-
Future 是惰性的还是立即执行的?
-
poll()返回Pending后,谁会再次调用poll()? -
async/await和 Future 是什么关系?
点击查看答案与解析
- 惰性的 - 需要被 poll 才会执行
- Waker(由执行器提供)
async/await是 Future 的语法糖,编译器转换为状态机
关键理解: Future 本身不执行,需要执行器 (Executor) 驱动。
延伸阅读
学习完 Futures 后,你可能还想了解:
- Future 组合子 - map, then, and_then
- Stream trait - 异步迭代器
- futures-util crate - 实用工具集
选择建议:
继续学习
前一章: Tokio 异步运行时
下一章: 宏编程
相关章节:
返回: 高级进阶
完整示例: futures_sample.rs
Rayon 数据并行库
开篇故事
想象你是一家大型工厂的厂长。工厂里有一堆零件需要加工,传统做法是让一个工人从头到尾完成所有零件。这个工人干得再快,也只是一个 worker 在干活。
现在换一种思路:你把零件分成若干份,让每个工人处理一小批。工人们各司其职,互不干扰,最后汇总结果。这就是并行计算的威力。
但是管理多个工人也有挑战:
- 如何分配任务?有的工人干得快,有的干得慢
- 如何避免有的工人闲着,有的工人忙不过来?
- 如何确保最终结果正确汇总?
Rayon 就是这个"智能工厂管理系统"——它是 Rust 生态中高性能的数据并行库,让你轻松地将顺序代码转换成并行代码,自动处理任务分配和负载均衡。最妙的是,它通过**工作窃取(Work Stealing)**算法,让空闲线程自动"偷取"忙碌线程的任务,确保所有 CPU 核心都被充分利用。
本章适合谁
如果你已经掌握了 Rust 基础,现在想要:
- 利用多核 CPU 加速数据处理
- 学习如何将顺序迭代器转换为并行迭代器
- 理解工作窃取调度算法的原理
- 掌握线程池和任务并行的高级用法
本章适合你。Rayon 的学习曲线非常平缓——很多时候,你只需要把 .iter() 改成 .par_iter(),就能立即获得并行加速。
你会学到什么
完成本章后,你可以:
- 解释什么是数据并行以及 Rayon 的核心优势
- 使用
par_iter()和par_iter_mut()进行并行迭代 - 使用
into_par_iter()进行所有权转移的并行处理 - 使用
join()并行执行两个独立任务 - 使用
scope()创建嵌套并行任务 - 理解工作窃取调度的工作原理
- 避免常见的并行编程错误
前置要求
学习本章前,你需要理解:
- 所有权 - 特别是移动语义和所有权转移
- 闭包 - 闭包作为并行操作的参数
- 迭代器 - 迭代器的基本用法
- 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!();
}
发生了什么?
use rayon::prelude::*导入 Rayon 的 trait,使.par_iter()方法可用.par_iter()创建一个并行迭代器.for_each()并行处理每个元素- 执行顺序不确定,但每个元素都会被处理
原理解析
数据并行 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:
| 特性 | Rayon | Tokio |
|---|---|---|
| 并行类型 | 数据并行(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_each 或 try_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
}
}
小结
核心要点:
- 数据并行是 Rayon 的核心——同一份操作在多个数据上并发执行
- 工作窃取算法自动负载均衡,空闲线程窃取忙碌线程的任务
- 三种迭代器:
par_iter()(借用)、par_iter_mut()(可变借用)、into_par_iter()(转移所有权) - 低开销:只需添加
par_前缀,大部分顺序代码可直接并行化 - 自动调度: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
延伸阅读
学习完并行计算后,你可能还想了解:
- rayon 官方文档 - 完整 API 参考
- 并行迭代器 - par_iter vs iter
- 工作窃取调度器 - 原理
选择建议:
继续学习
- 下一步:Tokio 异步运行时
- 进阶:线程与并发基础
- 回顾:闭包
💡 记住: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 万个浮点数进行复杂的数学计算
解析:
| 选项 | 推荐方案 | 原因 |
|---|---|---|
| A | Tokio/async | IO 密集型,需要异步等待 |
| B | Rayon | CPU 密集型,大数据集并行计算 |
| C | 顺序执行 | 用户交互,需要及时响应 |
| D | 连接池库 | 资源管理,非计算任务 |
Rayon 最适合数据并行场景——大数据集上的 CPU 密集型计算。
扩展阅读
官方资源
相关项目
- crossbeam - 并发编程工具集
- parking_lot - 高性能同步原语
进阶主题
- NUMA 感知调度:多路服务器的内存局部性优化
- SIMD 并行:结合
packed_simd进行向量化计算 - GPU 计算:结合
rust-gpu或cust进行异构并行
Mio 异步 I/O 库
开篇故事
想象你是一位邮局管理员,负责处理成千上万封信件。传统方式是依次处理每封信(阻塞 I/O):拿起一封信 → 读取 → 回复 → 放下 → 拿下一封。如果某封信需要等待回复,你就干等着,什么也做不了。
更聪明的做法是同时观察所有信箱,哪个有新信件就处理哪个(非阻塞 I/O)。这就是 Mio 的核心思想——它提供低级的异步 I/O 原语,是 Tokio 等高级运行时的基石。
本章适合谁
如果你想深入理解 Rust 异步 I/O 的底层实现原理,或者需要构建高性能的网络服务,本章适合你。Mio 是 Tokio、Hyper 等库的核心依赖。
你会学到什么
完成本章后,你可以:
- 理解 Mio 的设计理念:为什么它是"底层"的
- 使用 Poll、Token、Event 实现事件循环
- 构建非阻塞 TCP 服务器
- 理解 epoll/kqueue 等系统调用抽象
- 区分 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 源到 Pollpoll()- 阻塞等待事件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 对比
| 特性 | Mio | Tokio |
|---|---|---|
| 抽象级别 | 低级(接近系统调用) | 高级(异步运行时) |
| 编程模型 | 手动事件循环 | 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 标识。
小结
核心要点:
- Poll: 事件轮询器,封装系统调用
- Token: 唯一标识 I/O 源
- Event: 就绪事件
- Interest: 关注的事件类型
- 事件循环: 持续轮询和处理事件
关键术语:
- Event Loop: 事件循环
- Non-blocking I/O: 非阻塞 I/O
- epoll/kqueue/IOCP: 系统调用
- Token: 标识符
- Registry: 注册表
术语表
| English | 中文 |
|---|---|
| Event Loop | 事件循环 |
| Poll | 轮询器 |
| Token | 标识符 |
| Non-blocking I/O | 非阻塞 I/O |
| epoll | Linux 事件 |
| kqueue | macOS 事件 |
完整示例:src/advance/mio_sample.rs
知识检查
快速测验(答案在下方):
-
Mio 和 Tokio 是什么关系?
-
Interest::READABLE表示什么? -
为什么事件循环中不能有阻塞操作?
点击查看答案与解析
- Mio 是底层 I/O 抽象层,Tokio 在其上构建运行时
- 关注源的可读事件(有数据到达)
- 阻塞操作会阻止事件循环处理其他就绪事件
关键理解: Mio 提供原始 I/O 原语,Tokio 提供高级异步抽象。
继续学习
💡 记住:Mio 是异步 I/O 的基石。理解它,你就能理解所有异步运行时!
循环引用
开篇故事
想象你和朋友互相保管对方的钥匙:你把家门钥匙给他,他把家门钥匙给你。现在你们都被锁在外面了——因为要拿钥匙需要对方开门,但对方也需要你的钥匙才能开门。这就是循环引用的本质:两个对象互相持有对方的引用,永远无法释放。
在 Rust 中,Rc 和 Arc 是引用计数的智能指针,但单纯的引用计数无法检测循环引用。本章就是为了解决这个问题而设计的。
本章适合谁
如果你需要使用 Rc 或 Arc 构建复杂的数据结构(如图、树),担心循环引用导致内存泄漏,本章适合你。
你会学到什么
完成本章后,你可以:
- 理解循环引用的成因和危害
- 使用
Weak打破循环引用 - 识别何时会发生循环引用
- 设计避免循环的数据结构
前置要求
学习本章前,你需要理解:
- 所有权 - 理解所有权和借用
- 智能指针 - 理解
Rc和Arc - 引用计数 - 理解引用计数原理
依赖安装
运行以下命令安装所需依赖:
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: 创建双向链表
使用 Rc 和 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>>>,
}
// 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,可能存在循环。
小结
核心要点:
- 循环引用: 两个对象互相持有强引用,无法释放
- Weak 智能指针: 弱引用不增加计数,打破循环
- upgrade(): 安全地从弱引用获取强引用
- 设计模式: 单向强引用,反向弱引用
关键术语:
- 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
知识检查
快速测验(答案在下方):
-
为什么
Rc会导致循环引用问题? -
Weak::upgrade()返回什么类型? -
如何检测循环引用?
点击查看答案与解析
Rc是强引用,互相持有导致引用计数永远 > 0Option<Rc<T>>- 如果强引用还在返回Some,否则None- 使用
Weak弱引用打破循环,或使用调试工具检查引用计数
关键理解: 循环引用是 Rust 中少数会导致内存泄漏的情况。
延伸阅读
学习完循环引用检测后,你可能还想了解:
- Rc 和 Arc 的区别 - 何时使用哪个
- Weak 引用深入 - 弱引用原理
- 内存泄漏检测工具 - 自动化检测
选择建议:
继续学习
- 下一步:智能指针
- 进阶:无锁编程
- 回顾:所有权
💡 记住:循环引用是 Rust 中少数会导致内存泄漏的情况。使用 Weak 打破循环,确保内存安全!
数据库操作
开篇故事
想象你去餐厅吃饭。传统方式是:看菜单 → 告诉服务员 → 等待上菜。数据库编程就像这个过程:应用程序发出 SQL 查询 → 数据库执行 → 返回结果。Rust 的数据库库(SQLx 和 Diesel)就像智能点餐系统——在下单前就告诉你菜品是否存在、是否符合饮食限制。
在 Rust 中,数据库操作分为两大流派:SQLx(异步、编译时检查)和 Diesel(ORM、类型安全)。本章介绍这两种方法的核心概念。
本章适合谁
如果你需要在 Rust 程序中存储和检索数据,本章适合你。无论你是构建 Web 应用、CLI 工具还是微服务,数据库都是不可或缺的部分。
你会学到什么
完成本章后,你可以:
- 选择适合的数据库库(SQLx vs Diesel)
- 理解异步数据库操作的优势
- 掌握类型安全查询的原理
- 设计数据库连接管理策略
前置要求
学习本章前,你需要理解:
依赖安装
运行以下命令安装所需依赖:
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 对比
| 特性 | SQLx | Diesel |
|---|---|---|
| 查询方式 | 原生 SQL | DSL(领域特定语言) |
| 检查时机 | 编译时 | 编译时 |
| 异步支持 | ✅ 原生异步 | ❌ 同步(需手动包装) |
| 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: 确保:
- 数据库正在运行
- 连接字符串正确
- 表结构已创建
Q: Diesel 迁移如何管理?
A: 使用 Diesel CLI:
diesel migration generate create_users
diesel migration run
Q: 连接池耗尽如何处理?
A:
- 增加
max_connections - 检查是否有未释放的连接
- 使用连接池监控
小结
核心要点:
- SQLx: 异步、编译时检查、原生 SQL
- Diesel: ORM、类型安全、DSL 查询
- 连接池: 管理数据库连接复用
- 错误处理: 使用 Result 传播数据库错误
关键术语:
- Connection Pool: 连接池
- ORM: 对象关系映射
- Query Builder: 查询构建器
- Migration: 数据库迁移
下一步:
- 学习 SQLx 详细用法
- 探索 Diesel ORM
- 理解 事务管理
术语表
| English | 中文 |
|---|---|
| Connection Pool | 连接池 |
| ORM | 对象关系映射 |
| Query | 查询 |
| Transaction | 事务 |
| Migration | 迁移 |
| Prepared Statement | 预编译语句 |
完整示例:src/advance/database/sqlx_sample.rs, src/advance/database/diesel_sample.rs
知识检查
快速测验(答案在下方):
-
SQLx 和 Diesel 的核心区别是什么?
-
连接池的作用是什么?
-
编译时 SQL 检查是如何工作的?
点击查看答案与解析
- SQLx = 异步 + 原生 SQL,Diesel = 同步 + ORM
- 复用数据库连接,避免频繁创建/销毁
- 连接数据库验证 SQL 语法和表结构
关键理解: 选择数据库库取决于你的需求:异步用 SQLx,ORM 用 Diesel。
继续学习
- 下一步:SQLx 异步查询
- 进阶:Diesel ORM
- 回顾:异步编程
💡 记住:选择合适的数据库库取决于你的需求。复杂查询用 SQLx,业务逻辑用 Diesel!
Diesel ORM
开篇故事
想象你要写 SQL 查询。传统方式是:手写 SQL → 执行 → 手动映射结果 → 处理类型错误。Diesel 就像智能助手——你定义数据结构,它生成类型安全的 SQL,编译时就告诉你有没有错误。
本章适合谁
如果你需要在 Rust 程序中使用 ORM(对象关系映射)操作数据库,本章适合你。Diesel 是类型安全的 ORM,在编译时捕获 SQL 错误。
你会学到什么
完成本章后,你可以:
- 定义 Diesel Schema
- 创建数据模型结构体
- 执行 CRUD 操作
- 使用类型安全的查询构建器
- 处理数据库连接和事务
前置要求
- 结构体 - 结构体定义
- 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→ i32Text→ StringBool→ boolNullable<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)?;
小结
核心要点:
- Diesel: 类型安全的 ORM
- Schema: 使用 table! 宏定义
- Queryable: 自动映射查询结果
- Insertable: 类型安全的插入
- 查询构建器: filter, order, limit 等
- 事务: 使用 transaction 方法
关键术语:
- ORM (对象关系映射): 数据库表映射到结构体
- Schema: 数据库结构定义
- Queryable: 查询结果映射 trait
- Insertable: 插入数据 trait
- Transaction: 数据库事务
术语表
| English | 中文 |
|---|---|
| ORM | 对象关系映射 |
| Schema | 模式/架构 |
| Queryable | 可查询 |
| Insertable | 可插入 |
| Transaction | 事务 |
| Migration | 迁移 |
知识检查
快速测验(答案在下方):
-
Diesel 的 schema 是什么?
-
Queryable和Insertable有什么区别? -
如何处理数据库迁移?
点击查看答案与解析
- 数据库表结构的 Rust 表示(table! 宏)
Queryable= 查询结果映射,Insertable= 插入数据映射- 使用 Diesel CLI:
diesel migration generate和diesel migration run
关键理解: Diesel 是类型安全的 ORM,编译时检查查询。
继续学习
前一章: Futures 异步编程
下一章: 宏编程
相关章节:
- SQLx 数据库
- Futures 异步编程
- 序列化
返回: 高级进阶
完整示例: diesel_sample.rs
SQLx 数据库操作
开篇故事
想象你去银行办业务。传统方式是:排队 → 到窗口 → 说明需求 → 等待办理 → 完成。SQLx 就像银行的自助终端——你提交请求,它异步处理,完成后通知你。这样你不需要一直等待,可以同时处理其他事情。
本章适合谁
如果你需要在 Rust 程序中操作数据库(SQLite、MySQL、PostgreSQL 等),本章适合你。SQLx 是类型安全的异步数据库库,是构建数据驱动应用的首选。
你会学到什么
完成本章后,你可以:
- 使用 SQLx 连接数据库
- 执行 SQL 查询和插入操作
- 使用类型安全的查询绑定
- 将查询结果映射到结构体
- 使用连接池管理数据库连接
前置要求
- 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?;
小结
核心要点:
- SQLx: 异步数据库库,支持多种数据库
- query: 执行 SQL 查询
- bind: 参数绑定,防止 SQL 注入
- fetch_all/fetch_one: 获取查询结果
- FromRow: 自动映射到结构体
- 连接池: 管理多个连接,提高性能
关键术语:
- Connection (连接): 数据库连接
- Pool (连接池): 连接池管理
- Query (查询): SQL 查询
- Bind (绑定): 参数绑定
- FromRow: 结果映射 trait
术语表
| English | 中文 |
|---|---|
| Connection | 连接 |
| Connection Pool | 连接池 |
| Query | 查询 |
| Bind | 绑定 |
| Transaction | 事务 |
| Migration | 迁移 |
知识检查
快速测验(答案在下方):
-
query!和query_as!有什么区别? -
SQLx 的编译时检查需要什么条件?
-
如何处理可选参数?
点击查看答案与解析
query!返回匿名结构体,query_as!返回指定类型- 需要数据库连接和
DATABASE_URL环境变量 - 使用
Option<T>参数,SQL 中用IS NULL处理
关键理解: SQLx 在编译时验证 SQL,减少运行时错误。
继续学习
前一章: Tokio 异步运行时
下一章: Diesel ORM
相关章节:
- Tokio 异步运行时
- 数据库 ORM
- 序列化
返回: 高级进阶
完整示例: sqlx_sample.rs
Axum Web 框架
开篇故事
想象你开了一家餐厅。你需要:前台接待(路由)、服务员(处理器)、厨房(业务逻辑)、收银台(响应)。Axum 就像这个餐厅的完整管理系统——它帮你组织所有组件,让顾客(请求)得到高效服务。
本章适合谁
如果你想用 Rust 构建 Web 服务(REST API、Web 应用),本章适合你。Axum 是 Tokio 团队开发的 Web 框架,以类型安全、高性能、易用性著称。
你会学到什么
完成本章后,你可以:
- 创建 Axum Web 应用
- 定义路由和处理器
- 处理 JSON 请求和响应
- 实现优雅的服务器关闭
- 使用中间件和错误处理
前置要求
- 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())
}
}
}
小结
核心要点:
- Axum: 类型安全的 Web 框架
- Router: 定义路由
- Handler: 处理请求的函数
- Json: 自动序列化/反序列化
- Path: 提取路径参数
- 优雅关闭: 处理关闭信号
关键术语:
- Router (路由器): 路由管理
- Handler (处理器): 请求处理函数
- Middleware (中间件): 请求/响应拦截器
- Extractor (提取器): 从请求提取数据
- Graceful Shutdown (优雅关闭): 优雅处理关闭
术语表
| English | 中文 |
|---|---|
| Router | 路由器 |
| Handler | 处理器 |
| Middleware | 中间件 |
| Extractor | 提取器 |
| Graceful Shutdown | 优雅关闭 |
| Route | 路由 |
知识检查
快速测验(答案在下方):
-
Axum 和 Hyper 是什么关系?
-
路由参数如何提取?
-
中间件在 Axum 中如何实现?
点击查看答案与解析
- Axum 构建在 Hyper 之上,提供更高级的 API
- 使用
Path<T>提取器,T 需要实现Deserialize - 使用
tower::Service或axum::middleware
关键理解: Axum 是 Tokio 团队开发的 Web 框架。
继续学习
前一章: Tokio 异步运行时
下一章: HTTP 库
相关章节:
- Tokio 异步运行时
- HTTP 库
- 序列化
返回: 高级进阶
完整示例: axum_sample.rs
Hyper HTTP 库
开篇故事
想象你要建一家餐厅。Axum 是全套服务(前台、服务员、厨房),而 Hyper 只是厨房——它处理 HTTP 协议的核心部分,让你能构建自己的 Web 框架。Hyper 是 Rust 生态中许多 Web 框架的基础。
本章适合谁
如果你想深入理解 HTTP 协议底层,或想构建自己的 Web 框架,本章适合你。Hyper 是低级 HTTP 库,提供最大的灵活性。
你会学到什么
完成本章后,你可以:
- 理解 HTTP 请求和响应
- 创建 Hyper 服务器
- 处理请求路由
- 处理请求体和响应体
- 实现自定义服务
前置要求
- 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?;
小结
核心要点:
- Hyper: 低级 HTTP 库
- Request/Response: HTTP 请求和响应
- service_fn: 请求处理函数
- 路由: 手动匹配路径
- Body: 请求体和响应体处理
- 异步: 使用 Tokio 运行时
关键术语:
- HTTP: 超文本传输协议
- Request: HTTP 请求
- Response: HTTP 响应
- Body: 请求/响应体
- Service: 服务处理函数
术语表
| English | 中文 |
|---|---|
| HTTP | 超文本传输协议 |
| Request | 请求 |
| Response | 响应 |
| Body | 体/主体 |
| Service | 服务 |
| Route | 路由 |
知识检查
快速测验(答案在下方):
-
Hyper 和 Tokio 是什么关系?
-
Servicetrait 的作用是什么? -
什么时候应该直接使用 Hyper 而不是 Axum?
点击查看答案与解析
- Hyper 是基于 Tokio 的 HTTP 库
Service是处理请求/响应的抽象- 需要极致性能控制、自定义 HTTP 行为时
关键理解: Hyper 是底层 HTTP 库,Axum 是高级框架。
继续学习
前一章: Axum Web 框架
下一章: JSON 序列化
相关章节:
- Axum Web 框架
- Tokio 异步运行时
- JSON 序列化
返回: 高级进阶
完整示例: hyper_sample.rs
Ollama AI 集成
开篇故事
想象你要和一个 AI 聊天。传统方式是:调用复杂的 API → 处理响应 → 显示结果。Ollama 就像是:本地 AI 助手——在本地运行大语言模型,简单快速地集成 AI 功能。
本章适合谁
如果你想在 Rust 程序中集成 AI 功能(聊天机器人、文本生成),本章适合你。Ollama 是本地运行大语言模型的简单方式。
你会学到什么
完成本章后,你可以:
- 理解 Ollama 概念
- 连接 Ollama 服务
- 发送生成请求
- 处理 AI 响应
- 创建聊天机器人
前置要求
- 异步编程 - 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 数
小结
核心要点:
- Ollama: 本地 AI 运行库
- 默认连接: localhost:11434
- GenerationRequest: 生成请求
- 异步支持: 使用 async/await
- 聊天机器人: 简单实现
关键术语:
- Ollama: AI 运行平台
- Generation: 生成
- Model: 模型
- Prompt: 提示
术语表
| English | 中文 |
|---|---|
| Ollama | Ollama |
| Generation | 生成 |
| Model | 模型 |
| Prompt | 提示 |
| Chatbot | 聊天机器人 |
知识检查
快速测验(答案在下方):
-
Ollama 是什么?
-
如何在 Rust 中调用 Ollama API?
-
本地 AI 集成的应用场景有哪些?
点击查看答案与解析
- Ollama 是本地运行大语言模型的工具
- 使用 HTTP 客户端(如 reqwest)调用 Ollama 的 REST API
- 智能助手、代码生成、文本分析、摘要
关键理解: 本地 AI 集成保护隐私,无需云端依赖。
继续学习
前一章: 对象存储
下一章: 进程管理
相关章节:
- 对象存储
- 异步编程
- Tokio 运行时
返回: 高级进阶
完整示例: ollama_sample.rs
gRPC 服务
开篇故事
想象你在开发一个微服务系统。服务 A 需要调用服务 B 的函数,如果直接调用,两个服务必须部署在同一台机器上,使用相同的编程语言。但现实是:服务 A 用 Rust 编写,服务 B 用 Go 编写,它们运行在不同的服务器上。
gRPC 就像是一个"通用翻译器"——你定义服务接口(使用 Protocol Buffers),gRPC 自动生成客户端和服务端代码,让不同语言、不同机器的服务可以像本地调用一样通信。
本章适合谁
如果你想学习:
- 如何使用 gRPC 构建跨服务通信
- tonic 框架在 Rust 中的使用
- 如何定义和使用 Protocol Buffers
本章适合你。gRPC 是现代微服务架构的核心技术。
你会学到什么
完成本章后,你可以:
- 理解 gRPC 的核心概念(服务定义、Protocol Buffers、流式通信)
- 使用 tonic 创建 gRPC 服务端
- 使用 tonic 创建 gRPC 客户端
- 实现 Unary、Server Streaming、Client Streaming、Bidirectional Streaming
- 使用 clap 解析命令行参数
前置要求
依赖安装
运行以下命令安装所需依赖:
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)))
}
小结
核心要点:
- gRPC 是高性能、跨语言的 RPC 框架
- Protocol Buffers 是 gRPC 的接口定义语言
- tonic 是 Rust 的 gRPC 实现
- 四种通信模式: Unary、Server Streaming、Client Streaming、Bidirectional
- clap 用于解析命令行参数
关键术语:
| English | 中文 | 说明 |
|---|---|---|
| gRPC | gRPC 远程过程调用 | 高性能 RPC 框架 |
| Protocol Buffers | 协议缓冲区 | 接口定义语言 |
| tonic | tonic 框架 | Rust 的 gRPC 实现 |
| Unary RPC | 单次请求/响应 | 最简单的通信模式 |
| Streaming | 流式通信 | 多次请求/响应 |
| Service Discovery | 服务发现 | 动态查找服务地址 |
下一步:
- 学习 Unix Domain Socket - 本地进程间通信
- 了解 服务框架 - 生产级服务架构
- 探索 依赖注入 - 服务容器模式
术语表
| English | 中文 |
|---|---|
| gRPC | gRPC 远程过程调用 |
| Protocol Buffers | 协议缓冲区 |
| Service | 服务 |
| Method | 方法 |
| Request | 请求 |
| Response | 响应 |
| Streaming | 流式通信 |
| Unary | 单次请求/响应 |
| Bidirectional | 双向流式 |
完整示例:
继续学习
- 上一步:消息队列 - 异步通信
- 下一步:Unix Domain Socket - 本地 IPC
- 相关:服务框架 - 生产级服务架构
💡 记住:gRPC 的核心是"定义接口,自动生成代码"。使用 Protocol Buffers 定义服务,tonic 自动生成类型安全的客户端和服务端代码!
序列化基础
开篇故事
想象你要寄快递到不同国家。每个国家有不同的包装要求:有的用纸箱,有的用木箱,有的用塑料袋。但无论你用什么包装,里面的物品都是一样的。
在 Rust 中,Serde 框架就是你的"通用包装系统"——它定义了一套标准的序列化接口,然后针对不同格式(JSON、YAML、TOML、CSV、二进制)提供具体的"包装"实现。你只需定义一次数据结构,就能序列化成任何格式。
本章适合谁
如果你需要在 Rust 程序中序列化/反序列化数据,本章适合你。序列化是 Web API、配置文件、数据存储的基础。
你会学到什么
完成本章后,你可以:
- 理解 Serde 框架的工作原理
- 使用
#[derive(Serialize, Deserialize)]自动生成序列化代码 - 自定义序列化行为
- 使用 Serde 属性控制序列化行为
- 处理多种数据格式(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/Deserializetrait) - 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,
}
小结
核心要点:
- Serde 是框架:定义接口,格式库提供实现
- #[derive(Serialize, Deserialize)]: 自动生成代码
- 自定义序列化: 实现 trait
- Serde 属性: 控制序列化行为
- 多格式支持: 同一结构体可序列化成多种格式
关键术语:
| English | 中文 |
|---|---|
| Serialization | 序列化 |
| Deserialization | 反序列化 |
| Derive Macro | 派生宏 |
| Attribute | 属性 |
| Serializer | 序列化器 |
| Deserializer | 反序列化器 |
术语表
| English | 中文 |
|---|---|
| Serialization | 序列化 |
| Deserialization | 反序列化 |
| Derive Macro | 派生宏 |
| Attribute | 属性 |
| Transcode | 转码 |
| Zero-copy | 零拷贝 |
完整示例:src/advance/json_sample.rs
知识检查
快速测验(答案在下方):
-
#[derive(Serialize, Deserialize)]做了什么? -
如何处理序列化错误?
-
serde和serde_json的区别是什么?
点击查看答案与解析
- 自动生成序列化和反序列化代码
- 使用
Result<T, serde_json::Error>处理 serde是框架,serde_json是 JSON 实现
关键理解: Serde 是 Rust 序列化的事实标准框架,支持多种格式。
继续学习
💡 记住:Serde 让数据在任何地方都能安全"拆包"!
JSON 序列化
开篇故事
想象你要寄快递。你需要把物品打包成标准格式,贴上标签,才能通过快递系统运输。JSON 序列化就像这个打包过程——把你的 Rust 数据结构转换成标准的 JSON 格式,以便在不同系统间传输。
本章适合谁
如果你需要在 Rust 程序中处理 JSON 数据(读取配置文件、调用 API、存储数据),本章适合你。JSON 是现代编程中最常用的数据交换格式。
你会学到什么
完成本章后,你可以:
- 使用 serde_json 解析 JSON 数据
- 将 Rust 结构体序列化为 JSON
- 处理无类型和有类型的 JSON 数据
- 处理 JSON 错误和验证
- 自定义序列化和反序列化行为
前置要求
依赖安装
运行以下命令安装所需依赖:
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);
}
}
}
}
小结
核心要点:
- serde + serde_json: Rust 的 JSON 处理标准
- Value vs Struct: 无类型 vs 有类型解析
- derive 宏: 自动实现 Serialize/Deserialize
- 错误处理: 使用 Result 处理解析错误
- 自定义行为: 使用 serde 属性自定义序列化
关键术语:
- Serialization (序列化): Rust 结构体 → JSON
- Deserialization (反序列化): JSON → Rust 结构体
- Value: 无类型的 JSON 值
- derive macro: 自动派生 trait 实现
术语表
| English | 中文 |
|---|---|
| Serialization | 序列化 |
| Deserialization | 反序列化 |
| Serialize | 序列化 trait |
| Deserialize | 反序列化 trait |
| derive macro | 派生宏 |
| Value | 值类型 |
知识检查
快速测验(答案在下方):
-
serde_json::Value和类型化反序列化有什么区别? -
如何处理 JSON 中可能缺失的字段?
-
#[serde(flatten)]的作用是什么?
点击查看答案与解析
Value是无类型的动态 JSON 对象,类型化反序列化直接得到结构体- 使用
Option<T>字段,或#[serde(default)] - 将嵌套对象"展平"到当前结构体中
关键理解: 优先使用类型化反序列化,Value 仅在动态场景使用。
继续学习
前一章: Hyper HTTP 库
下一章: CSV 处理
相关章节:
返回: 高级进阶
完整示例: json_sample.rs
CSV 文件处理
开篇故事
想象你收到一份 Excel 表格,里面是员工信息:ID、姓名、年龄、部门、薪资。你需要筛选出薪资大于 5000 且年龄小于 50 的员工,然后保存到新文件。手动操作很繁琐,而 Rust 的 CSV 库可以自动完成这个任务。
本章适合谁
如果你需要在 Rust 程序中处理 CSV 文件(读取数据、分析数据、导出数据),本章适合你。CSV 是最常用的数据交换格式之一。
你会学到什么
完成本章后,你可以:
- 读取和解析 CSV 文件
- 将 CSV 数据映射到结构体
- 写入 CSV 数据
- 处理 CSV 错误和验证
- 处理大型 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?);
}
小结
核心要点:
- csv crate: CSV 处理标准库
- Serde 集成: 自动映射到结构体
- Reader/Writer: 读写 CSV
- 错误处理: 使用 Result 处理解析错误
- 大文件处理: 逐行处理避免内存溢出
关键术语:
- CSV: 逗号分隔值
- Deserialize: 反序列化
- Serialize: 序列化
- Reader: CSV 读取器
- Writer: CSV 写入器
术语表
| English | 中文 |
|---|---|
| CSV | 逗号分隔值 |
| Deserialize | 反序列化 |
| Serialize | 序列化 |
| Reader | 读取器 |
| Writer | 写入器 |
| Delimiter | 分隔符 |
知识检查
快速测验(答案在下方):
-
CSV 和 JSON 序列化有什么区别?
-
如何处理 CSV 中不同类型的列?
-
csv::Reader和csv::StringRecord的区别?
点击查看答案与解析
- CSV 是表格格式(行/列),JSON 是树形格式
- 使用 Serde 反序列化到结构体,自动类型转换
Reader是迭代器,StringRecord是单行数据
关键理解: CSV 适合表格数据,JSON 适合嵌套数据。
继续学习
相关章节:
返回: 高级进阶
完整示例: csv_sample.rs
Rkyv 零拷贝序列化
开篇故事
想象你要寄一本很厚的书。传统方式是:复印整本书 → 打包 → 邮寄 → 收件人阅读。零拷贝就像是:直接把书递给收件人,不需要复印。Rkyv 就是这样的零拷贝序列化库,特别适合大数据集。
本章适合谁
如果你需要高性能序列化(处理大数据集、网络传输),本章适合你。Rkyv 是零拷贝序列化库,性能远超传统序列化方法。
你会学到什么
完成本章后,你可以:
- 理解零拷贝序列化概念
- 使用 rkyv 序列化和反序列化
- 使用归档类型 (Archived types)
- 自定义序列化配置
- 处理复杂数据结构
前置要求
- 结构体 - 结构体定义
- 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,
}
生成的归档类型:
Person→ArchivedPerson- 可以直接访问字段,无需反序列化
序列化
简单序列化:
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 不支持循环引用。使用 Rc 或 Arc 重构数据结构。
知识扩展
自定义分配器
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();
小结
核心要点:
- Rkyv: 零拷贝序列化库
- Archive: 归档类型,直接访问内存
- 零拷贝: 无需反序列化即可访问数据
- 高性能: 远超传统序列化方法
- 缓冲区: 需要足够大小的缓冲区
关键术语:
- Zero-copy (零拷贝): 无需复制即可访问数据
- Archive (归档): 序列化后的内存布局
- Archived type (归档类型): 自动生成的归档类型
- Serializer (序列化器): 序列化数据的工具
术语表
| English | 中文 |
|---|---|
| Zero-copy | 零拷贝 |
| Archive | 归档 |
| Archived type | 归档类型 |
| Serializer | 序列化器 |
| Deserializer | 反序列化器 |
| Buffer | 缓冲区 |
知识检查
快速测验(答案在下方):
-
rkyv 和 serde 的主要区别是什么?
-
什么是"零拷贝"反序列化?
-
什么时候应该使用 rkyv 而不是 serde?
点击查看答案与解析
- rkyv 是零拷贝序列化,serde 需要反序列化到内存
- 直接访问序列化后的内存,无需复制到新结构
- 性能关键场景:游戏、数据库、网络传输大数据
关键理解: rkyv 牺牲兼容性换取极致性能。
继续学习
前一章: CSV 处理
下一章: 临时文件
相关章节:
返回: 高级进阶
完整示例: rkyv_sample.rs
文件与目录操作
开篇故事
想象你在整理一个巨大的仓库。传统方式是:走进仓库 → 找到物品 → 拿出来 → 走回办公室记录。每次只能处理一件物品,效率极低。
更聪明的做法是:使用智能仓库管理系统——你可以一次性列出所有物品、批量移动、按类别搜索,甚至在不同房间之间建立快捷通道。Rust 的文件与目录操作就是你的"智能仓库管理系统"——它让你高效地管理文件系统。
本章适合谁
如果你需要在 Rust 程序中读写文件、遍历目录、处理路径,本章适合你。文件系统操作是几乎所有应用程序的基础需求。
你会学到什么
完成本章后,你可以:
- 使用
std::fs读写文件 - 使用
std::path::Path和PathBuf处理路径 - 遍历目录树
- 创建和删除文件/目录
- 获取文件和目录元数据
- 处理跨平台路径差异
前置要求
依赖安装
运行以下命令安装所需依赖:
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: 使用 BufReader 和 BufWriter 逐行/逐块处理:
use std::io::{BufReader, BufRead};
let file = File::open("large.txt")?;
let reader = BufReader::new(file);
for line in reader.lines() {
// 处理每一行
}
Q: 跨平台路径分隔符?
A: 永远使用 Path::join 或 PathBuf::push,不要硬编码 / 或 \。
小结
核心要点:
- std::fs: 文件和目录操作
- Path/PathBuf: 路径处理
- read_dir: 目录遍历
- metadata: 文件信息
术语表
| English | 中文 |
|---|---|
| File System | 文件系统 |
| Path | 路径 |
| Directory | 目录 |
| Metadata | 元数据 |
| Recursion | 递归 |
| Cross-platform | 跨平台 |
完整示例:src/advance/system/directory_sample.rs
知识检查
快速测验(答案在下方):
-
Path和PathBuf有什么区别? -
为什么不应该使用字符串拼接路径?
-
fs::read_dir返回什么类型?
点击查看答案与解析
Path是借用类型,PathBuf是拥有类型- 跨平台不兼容(Windows 用
\,Unix 用/) Result<ReadDir, io::Error>,迭代返回Result<DirEntry, io::Error>
关键理解: 始终使用 Path::join 构建路径。
继续学习
💡 记住:始终使用 Path/PathBuf 处理路径,确保跨平台兼容!
临时文件处理
开篇故事
想象你在写一份草稿,需要临时保存一下。你不会把它放进永久文件夹,而是放在桌面的临时区域,用完就清理。tempfile 库就像这个临时区域——帮你创建临时文件和目录,用完自动清理。
本章适合谁
如果你需要在 Rust 程序中创建临时文件(缓存数据、中间结果、测试数据),本章适合你。临时文件是处理大量数据或测试的常用技术。
你会学到什么
完成本章后,你可以:
- 创建临时文件
- 创建临时目录
- 读写临时文件
- 自动清理临时资源
- 处理临时文件错误
前置要求
依赖安装
运行以下命令安装所需依赖:
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)?;
小结
核心要点:
- tempfile: 自动清理临时文件
- NamedTempFile: 可获取路径的临时文件
- tempdir: 临时目录支持
- 自动清理: 离开作用域自动删除
- reopen(): 重新打开临时文件
关键术语:
- Temporary File (临时文件): 临时存储数据的文件
- NamedTempFile: 命名临时文件
- TempDir: 临时目录
- Auto-cleanup: 自动清理
术语表
| English | 中文 |
|---|---|
| Temporary File | 临时文件 |
| NamedTempFile | 命名临时文件 |
| TempDir | 临时目录 |
| Auto-cleanup | 自动清理 |
| Scope | 作用域 |
知识检查
快速测验(答案在下方):
-
tempfile()和NamedTempFile有什么区别? -
临时文件什么时候被删除?
-
TempDir的作用是什么?
点击查看答案与解析
tempfile()无名称(自动删除),NamedTempFile有路径(可持久化)TempFile在 drop 时自动删除,NamedTempFile可调用persist()保留TempDir创建临时目录,drop 时删除目录及其内容
关键理解: 临时文件是测试和临时数据存储的理想选择。
继续学习
相关章节:
返回: 高级进阶
完整示例: tempfile_sample.rs
内存映射文件
开篇故事
想象你要读一本很厚的书。传统方式是:一页一页读 → 记住内容 → 合上书。内存映射就像是:把整本书摊开在桌子上,你可以直接翻阅任何一页,不需要逐页读取。memmap 就是这样的技术,特别适合大文件处理。
本章适合谁
如果你需要高效处理大文件(数据库文件、日志文件、大数据集),本章适合你。内存映射是高性能文件处理的关键技术。
你会学到什么
完成本章后,你可以:
- 理解内存映射概念
- 将文件映射到内存
- 直接访问文件内容
- 修改内存映射内容
- 获取系统页面大小
前置要求
依赖安装
运行以下命令安装所需依赖:
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)? };
小结
核心要点:
- 内存映射: 将文件映射到内存地址空间
- Mmap: 只读内存映射
- MmapMut: 可写内存映射
- 零拷贝: 直接访问文件内容
- 性能优势: 大文件处理更高效
关键术语:
- Memory Map (内存映射): 文件到内存的映射
- Mmap: 只读映射类型
- MmapMut: 可写映射类型
- Zero-copy (零拷贝): 无需数据复制
术语表
| English | 中文 |
|---|---|
| Memory Map | 内存映射 |
| Mmap | 内存映射(只读) |
| MmapMut | 内存映射(可写) |
| Zero-copy | 零拷贝 |
| Page Size | 页面大小 |
知识检查
快速测验(答案在下方):
-
内存映射和普通文件读写有什么区别?
-
什么时候应该使用内存映射?
-
MmapMut和Mmap的区别?
点击查看答案与解析
- 内存映射将文件映射到内存,直接访问无需系统调用
- 大文件随机访问、进程间共享内存
MmapMut可写,Mmap只读
关键理解: 内存映射适合大文件,小文件用普通 I/O 更简单。
继续学习
相关章节:
返回: 高级进阶
完整示例: memmap_sample.rs
环境变量配置
开篇故事
想象你要配置一个应用程序。硬编码配置(如数据库密码)就像把密码写在代码里——不安全且难以修改。环境变量就像配置文件——可以随时修改而不需要重新编译。.env 文件就是这样的配置文件。
本章适合谁
如果你需要在 Rust 程序中管理配置(数据库连接、API 密钥、环境设置),本章适合你。环境变量是管理配置的标准方法。
你会学到什么
完成本章后,你可以:
- 从 .env 文件加载环境变量
- 读取环境变量
- 获取系统目录路径
- 构建相对路径
- 处理环境变量错误
前置要求
- 错误处理 - 错误处理基础
- 文件与目录操作 - 文件路径基础
依赖安装
运行以下命令安装所需依赖:
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()?,
})
}
}
小结
核心要点:
- dotenvy: 从 .env 文件加载环境变量
- env::var(): 读取环境变量
- home_dir(): 获取用户主目录
- CARGO_MANIFEST_DIR: 项目根目录
- PathBuf: 构建跨平台路径
关键术语:
- .env File: 环境变量配置文件
- Environment Variable: 环境变量
- PathBuf: 路径缓冲区
- Manifest Dir: 清单目录(项目根目录)
术语表
| English | 中文 |
|---|---|
| Environment Variable | 环境变量 |
| .env File | .env 文件 |
| PathBuf | 路径缓冲区 |
| Manifest Directory | 清单目录 |
| Home Directory | 主目录 |
知识检查
快速测验(答案在下方):
-
.env文件应该在什么时候加载? -
环境变量和配置文件的区别?
-
如何处理不同环境的配置?
点击查看答案与解析
- 程序启动时,在读取环境变量之前
- 环境变量是进程级别的,配置文件是文件持久化的
- 使用
.env.development,.env.production或配置管理库
关键理解: .env 文件不应提交到版本控制(包含敏感信息)。
继续学习
相关章节:
返回: 高级进阶
完整示例: dotenv_sample.rs
字节处理
开篇故事
想象你要处理二进制数据(图片、音频、网络数据包)。直接操作字节数组就像用手抓沙子——容易散落。bytes 库就像容器——帮你安全、高效地管理字节数据。
本章适合谁
如果你需要处理二进制数据(网络编程、文件处理、加密),本章适合你。bytes 是高性能字节处理的标准库。
你会学到什么
完成本章后,你可以:
- 创建 Bytes 和 BytesMut
- 分割和合并字节
- 实现 Buf 和 BufMut trait
- 使用 Base64 编解码
- 使用位向量 (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?;
小结
核心要点:
- Bytes: 零拷贝字节容器
- BytesMut: 可写字节缓冲区
- Buf/BufMut: 字节读取/写入 trait
- Base64: 二进制到文本编码
- BitVec: 位级别操作
关键术语:
- Bytes: 字节类型
- Buf: 缓冲区 trait
- Zero-copy: 零拷贝
- Base64: Base64 编码
- BitVec: 位向量
术语表
| English | 中文 |
|---|---|
| Bytes | 字节 |
| Buffer | 缓冲区 |
| Zero-copy | 零拷贝 |
| Base64 | Base64 编码 |
| BitVec | 位向量 |
| Endianness | 字节序 |
延伸阅读
学习完字节处理后,你可能还想了解:
- Bytes crate - 零拷贝字节缓冲区
- 零拷贝网络编程 - 直接传递缓冲区
- SIMD 字节处理 - 加速解析
选择建议:
知识检查
快速测验(答案在下方):
-
&[u8]和Vec<u8>的区别? -
如何高效地拼接字节缓冲区?
-
Bytescrate 的优势是什么?
点击查看答案与解析
&[u8]是借用切片,Vec<u8>是拥有的向量- 使用
Vec::extend或预分配容量 - 引用计数、零拷贝克隆、高效分割
关键理解: 字节处理是网络和系统编程的基础。
继续学习
相关章节:
返回: 高级进阶
完整示例: bytes_sample.rs
Cow 类型
开篇故事
想象你有一本书要修改。传统方式是:复印整本书 → 修改复印件 → 使用。Cow 就像是:如果需要修改才复印,如果不需要修改直接看原书。Cow 类型就是这样的智能类型——按需克隆。
本章适合谁
如果你需要优化内存使用(只读时借用,修改时克隆),本章适合你。Cow 是 Rust 特有的零成本抽象。
你会学到什么
完成本章后,你可以:
- 理解 Cow 类型概念
- 使用 Cow::Borrowed 和 Cow::Owned
- 使用 to_mut() 按需克隆
- 优化字符串处理
- 实现零拷贝优化
前置要求
- 所有权 - 所有权基础
- 借用 - 借用基础
- 枚举 - 枚举类型
第一个例子
最简单的 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 可以用于任何实现
ToOwnedtrait 的类型 - 常见:
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()) // 需要转换,分配
}
}
小结
核心要点:
- Cow: 写时克隆类型
- Borrowed: 借用,零分配
- Owned: 拥有,分配内存
- to_mut(): 按需克隆
- 零成本: 只读场景无开销
关键术语:
- Cow (Clone-on-Write): 写时克隆
- Borrowed: 借用变体
- Owned: 拥有变体
- Zero-copy: 零拷贝
术语表
| English | 中文 |
|---|---|
| Cow (Clone-on-Write) | 写时克隆 |
| Borrowed | 借用 |
| Owned | 拥有 |
| ToOwned trait | ToOwned trait |
| Zero-copy | 零拷贝 |
延伸阅读
学习完 Cow 类型后,你可能还想了解:
选择建议:
知识检查
快速测验(答案在下方):
-
Cow<'a, str>的三个变体是什么? -
什么时候会发生克隆?
-
Cow的使用场景?
点击查看答案与解析
Borrowed(&'a str)和Owned(String)- 当需要修改借用数据时
- 读多写少场景,如配置处理、字符串处理
关键理解: Cow 是写时复制的零成本抽象。
继续学习
前一章: 字节处理
下一章: 派生宏
相关章节:
- 字节处理
- 所有权
- 借用
返回: 高级进阶
完整示例: cow_sample.rs
进程管理
开篇故事
想象你要运行一个外部程序。传统方式是:手动启动 → 等待完成 → 获取结果。process 库就像是:程序管家——帮你启动、管理、监控外部进程。
本章适合谁
如果你需要在 Rust 程序中运行外部命令、管理子进程,本章适合你。进程管理是系统编程的基础。
你会学到什么
完成本章后,你可以:
- 获取当前进程 ID
- 启动子进程
- 管理进程生命周期
- 捕获进程输出
- 处理进程错误
前置要求
- 错误处理 - 错误处理基础
- 文件与目录操作 - 文件路径基础
依赖安装
运行以下命令安装所需依赖:
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));
}
小结
核心要点:
- process::id(): 获取进程 ID
- Command: 启动子进程
- output(): 同步执行
- spawn(): 异步执行
- wait(): 等待进程完成
关键术语:
- Process: 进程
- PID: 进程 ID
- Command: 命令
- Spawn: 派生
术语表
| English | 中文 |
|---|---|
| Process | 进程 |
| PID | 进程 ID |
| Command | 命令 |
| Spawn | 派生 |
| Output | 输出 |
知识检查
快速测验(答案在下方):
-
Command::spawn()和Command::output()有什么区别? -
如何向子进程发送数据?
-
子进程退出后如何获取退出码?
点击查看答案与解析
spawn()返回正在运行的子进程,output()等待完成并返回输出- 使用
stdin(Stdio::piped())获取 stdin 句柄 Child::wait()返回ExitStatus,使用.code()获取退出码
关键理解: spawn() 适合长时间运行的进程,output() 适合一次性命令。
继续学习
前一章: Ollama AI 集成
下一章: 系统信息
相关章节:
- Ollama AI 集成
- 系统信息
- 错误处理
返回: 高级进阶
完整示例: process_sample.rs
系统信息
开篇故事
想象你要检查电脑的健康状况。传统方式是:打开各种工具 → 查看 CPU → 查看内存 → 查看进程。sysinfo 库就像是:系统仪表盘——一个库获取所有系统信息。
本章适合谁
如果你需要在 Rust 程序中获取系统信息(CPU、内存、进程),本章适合你。sysinfo 是跨平台系统监控的标准库。
你会学到什么
完成本章后,你可以:
- 获取系统信息
- 获取内存使用情况
- 获取进程列表
- 监控特定进程
- 获取 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());
}
小结
核心要点:
- System: 系统信息
- SystemExt: 系统扩展 trait
- 进程监控: 获取进程信息
- 跨平台: 支持多平台
关键术语:
- System: 系统
- Process: 进程
- PID: 进程 ID
- Memory: 内存
术语表
| English | 中文 |
|---|---|
| System | 系统 |
| Process | 进程 |
| PID | 进程 ID |
| Memory | 内存 |
| CPU | 中央处理器 |
知识检查
快速测验(答案在下方):
-
System::new_all()做了什么? -
如何获取特定进程的信息?
-
refresh_all()和new_all()的区别?
点击查看答案与解析
- 创建 System 实例并刷新所有信息(CPU、内存、进程等)
- 使用
system.process(Pid::from_u32(pid)) new_all()= 创建 + 刷新,refresh_all()= 仅刷新已有实例
关键理解: sysinfo 提供跨平台的系统和进程信息。
继续学习
相关章节:
返回: 高级进阶
完整示例: sysinfo_sample.rs
资源嵌入
开篇故事
想象你要打包应用程序的资源文件(图片、配置文件)。传统方式是:读取文件 → 打包 → 运行时加载。include_dir 就像是:时间胶囊——在编译时就把文件打包进程序,运行时直接使用。
本章适合谁
如果你想在 Rust 程序中嵌入静态资源(配置文件、图片、数据),本章适合你。include_dir 是编译时资源嵌入的标准库。
你会学到什么
完成本章后,你可以:
- 理解 include_dir 概念
- 嵌入目录到二进制
- 访问嵌入的文件
- 遍历目录结构
- 处理二进制文件
前置要求
- 文件与目录操作 - 文件路径基础
- 宏编程 - 宏基础
第一个例子
最简单的 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");
}
小结
核心要点:
- include_dir: 嵌入目录
- 编译时嵌入: 零运行时开销
- 类型安全: 编译时检查
- 遍历支持: 递归遍历目录
- 二进制支持: 支持文本和二进制
关键术语:
- Include Dir: 包含目录
- Embed: 嵌入
- Compile-time: 编译时
- Runtime: 运行时
术语表
| English | 中文 |
|---|---|
| Include Dir | 包含目录 |
| Embed | 嵌入 |
| Compile-time | 编译时 |
| Runtime | 运行时 |
| Binary | 二进制 |
延伸阅读
学习完资源嵌入后,你可能还想了解:
- include_bytes! - 嵌入二进制
- rust-embed crate - 更强大的嵌入
- Phf (完美哈希) - 编译时查找表
选择建议:
知识检查
快速测验(答案在下方):
-
include_str!和include_bytes!的区别? -
编译时嵌入的优缺点?
-
rust-embedcrate 的作用?
点击查看答案与解析
include_str!返回&str,include_bytes!返回&[u8]- 优点:无需运行时加载,缺点:增加二进制大小
- 更强大的嵌入,支持目录和 glob 模式
关键理解: 编译时嵌入适合小型静态资源。
继续学习
前一章: 系统信息
下一章: 对象存储
相关章节:
- 系统信息
- 对象存储
- 宏编程
返回: 高级进阶
完整示例: includedir_sample.rs
Unix Domain Socket (UDS)
开篇故事
想象你在一家大公司工作。如果两个部门需要通信,传统方式是打电话(网络 Socket)——即使他们在同一栋楼里。但更高效的方式是直接走到对方办公室(Unix Domain Socket)——因为他们在同一台机器上,无需经过外部网络。
Unix Domain Socket(UDS)是同一台机器上进程间通信(IPC)的高效方式。它使用文件系统路径作为地址,比 TCP/IP 快得多,因为数据不需要经过网络协议栈。
本章适合谁
如果你想学习:
- 如何在同一台机器上实现高效的进程间通信
- Unix Domain Socket 的工作原理
- 如何设计自定义二进制协议
本章适合你。UDS 是本地 IPC 的首选方案。
你会学到什么
完成本章后,你可以:
- 理解 Unix Domain Socket 的核心概念
- 使用
std::os::unix::net创建 UDS 服务端 - 使用
std::os::unix::net创建 UDS 客户端 - 实现自定义二进制协议(长度前缀 + 负载)
- 使用父进程编排多个子进程
前置要求
依赖安装
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_server和cargo 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 对比
| 特性 | UDS | TCP |
|---|---|---|
| 通信范围 | 同一台机器 | 跨网络 |
| 性能 | 更快(无需网络协议栈) | 较慢 |
| 地址格式 | 文件系统路径 | 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(())
}
小结
核心要点:
- UDS 是同一台机器上进程间通信的高效方式
- 自定义协议 使用长度前缀避免粘包问题
- 父进程编排 使用
Command启动和管理子进程 - 清理 socket 文件 是必要的,否则绑定会失败
关键术语:
| English | 中文 | 说明 |
|---|---|---|
| Unix Domain Socket | Unix 域套接字 | 本地 IPC 机制 |
| Length Prefix | 长度前缀 | 避免粘包问题 |
| Parent Process | 父进程 | 编排子进程的进程 |
| Child Process | 子进程 | 被父进程启动的进程 |
| Big-Endian | 大端序 | 网络字节序 |
下一步:
术语表
| English | 中文 |
|---|---|
| Unix Domain Socket | Unix 域套接字 |
| 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 方案。
你会学到什么
完成本章后,你可以:
- 理解 Stdio IPC 的核心概念
- 使用
std::process::Command启动子进程 - 使用
Stdio::piped()创建管道 - 实现父进程与子进程的双向通信
- 处理子进程的生命周期
前置要求
依赖安装
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 IPC | UDS |
|---|---|---|
| 复杂度 | 简单 | 中等 |
| 协议 | 行分隔文本 | 自定义二进制 |
| 适用场景 | 父子进程 | 任意进程 |
| 性能 | 中等 | 高 |
| 跨平台 | ✅ 是 | ❌ 仅 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(())
}
小结
核心要点:
- Stdio IPC 是最简单的进程间通信方式
- 管道 通过
Stdio::piped()创建 - 刷新缓冲区 是必要的,否则数据不会发送
- 关闭 stdin 通知子进程结束
关键术语:
| English | 中文 | 说明 |
|---|---|---|
| Stdio | 标准输入输出 | stdin/stdout/stderr |
| Pipe | 管道 | 进程间通信通道 |
| Parent Process | 父进程 | 启动子进程的进程 |
| Child Process | 子进程 | 被启动的进程 |
| Flush | 刷新 | 将缓冲区数据写入 |
下一步:
- 学习 Unix Domain Socket - 更高效的本地 IPC
- 了解 gRPC 服务 - 跨网络 RPC 通信
- 探索 进程管理 - 进程控制基础
术语表
| English | 中文 |
|---|---|
| Stdio | 标准输入输出 |
| Pipe | 管道 |
| Parent Process | 父进程 |
| Child Process | 子进程 |
| Flush | 刷新 |
| BufReader | 缓冲读取器 |
| Spawn | 启动(进程) |
| Wait | 等待(进程结束) |
完整示例:
继续学习
- 上一步:Unix Domain Socket - 本地 IPC
- 下一步:gRPC 服务 - 跨网络 RPC 通信
- 相关:进程管理 - 进程控制基础
💡 记住:Stdio IPC 简单但强大。对于父子进程通信,优先选择 Stdio;对于任意进程间通信,选择 UDS 或 gRPC!
CLI 开发最佳实践
开篇故事
想象你在设计一个瑞士军刀。如果每个工具都混在一起,用户会很难找到需要的功能。但如果每个工具都有清晰的标签、合理的位置,用户就能快速找到并使用。
CLI(命令行界面)工具就是程序的"瑞士军刀"。好的 CLI 工具应该:参数清晰、帮助文档完整、错误信息友好、支持子命令组织功能。在 Rust 生态中,clap 是最流行的 CLI 参数解析库,它让构建专业的 CLI 工具变得简单。
本章适合谁
如果你想学习:
- 如何使用 clap 构建专业的 CLI 工具
- 如何设计子命令结构
- CLI 项目的最佳实践
本章适合你。CLI 开发是 Rust 最常见的应用场景之一。
你会学到什么
完成本章后,你可以:
- 使用 clap 解析命令行参数
- 设计子命令结构组织复杂功能
- 实现
--help和--version支持 - 处理 CLI 错误并输出友好信息
- 设计符合 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 哲学
- 每个程序只做一件事
- 程序之间通过文本流通信
- 快速原型,使用文本流
好的 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));
}
}
}
小结
核心要点:
- clap 是 Rust 最流行的 CLI 参数解析库
- 派生宏 让参数定义变得简单
- 子命令 帮助组织复杂功能
- 错误处理 使用 anyhow 提供友好上下文
- 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 Invalidation | Iterator 失效 |
| Undefined Behavior (UB) | 未定义行为 |
完整示例:crates/awesome/src/
继续学习
- 上一步:所有权 - Rust 内存安全的核心
- 下一步:错误处理 - Rust 错误处理最佳实践
- 相关:指针与 unsafe - 何时需要 unsafe
💡 记住:Rust 的编译时检查不是限制你,而是保护你。每次编译器报错,它都在帮你防止一个潜在的运行时 bug!
原子类型 (Atomic Types)
开篇故事
想象你在银行柜台办理业务。如果只有一个柜台,所有人必须排队等待(Mutex 锁)。如果银行开了多个窗口,并且有一个电子显示屏显示当前排队号码,每个人都可以查看号码并决定何时去办理,而不需要死等。
Rust 的原子类型就像那个电子显示屏。它们允许你在多线程环境下安全地共享和修改数据,而不需要加锁。这是实现高性能并发编程的关键。
本章适合谁
如果你已经理解了基本的并发概念(如线程、Mutex),现在想进一步提升并发性能,或者对无锁编程感兴趣,本章适合你。
你会学到什么
完成本章后,你可以:
- 理解原子操作的概念
- 使用
std::sync::atomic模块中的原子类型 - 理解内存序 (Memory Ordering) 的基本概念
- 对比原子类型与 Mutex 的性能差异
- 实现简单的无锁计数器
前置要求
第一个例子
使用 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)
内存序定义了原子操作如何影响内存的可见性。这是原子类型中最难理解的部分。
常见内存序:
- Relaxed: 只保证原子性,不保证顺序。适用于计数器。
- Acquire: 保证此操作之后的读写不会被重排到操作之前。用于获取锁。
- Release: 保证此操作之前的读写不会被重排到操作之后。用于释放锁。
- AcqRel: 同时包含 Acquire 和 Release。用于读-改-写操作。
- 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)); }
小结
核心要点:
- 原子类型: 硬件级原子操作,无锁。
- 适用场景: 简单状态、计数器、标志位。
- 内存序:
Relaxed(计数器),Acquire/Release(同步),SeqCst(默认)。 - 对比 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 | 顺序一致性 |
延伸阅读
- Rust Book - Atomic Types
- C++ Memory Model - Rust 内存序基于 C++ 模型
继续学习
知识检查
问题 1 🟢 (基础)
AtomicUsize 主要适用于什么场景?
A) 保护复杂的 HashMap
B) 线程安全的计数器或标志位
C) 替代所有的 Mutex
D) 存储大对象
点击查看答案
答案: B) 线程安全的计数器或标志位
问题 2 🟡 (中等)
以下哪种内存序最严格?
A) Relaxed
B) Acquire
C) Release
D) SeqCst
点击查看答案
答案: D) SeqCst (顺序一致性)
测试基础
开篇故事
想象你在建造一座大桥。你不会等到桥建好了才测试它是否稳固——你会在每一步都进行检查:地基是否牢固?钢筋强度够吗?混凝土配比正确吗?软件测试也是如此。测试不是最后才做的事情,而是贯穿整个开发过程的质量保障。
Rust 的测试系统就像一位严格的质检员——它在编译时就确保你的代码符合预期,让 bug 无处藏身。
本章适合谁
如果你想学习如何编写可靠的 Rust 代码,或者理解测试在 Rust 中的最佳实践,本章适合你。
你会学到什么
完成本章后,你可以:
- 理解 Rust 测试的三种类型(单元、集成、文档)
- 使用
#[cfg(test)]组织测试模块 - 使用
assert!、assert_eq!、assert_ne!宏 - 编写会 panic 的测试 (
#[should_panic]) - 使用
#[ignore]跳过慢测试 - 运行特定测试和并行测试
前置要求
第一个例子
最简单的测试:
#![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 test | cargo 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
小结
核心要点:
- #[cfg(test)]: 只在测试时编译
- #[test]: 标记测试函数
- Assert 宏: 验证预期结果
- should_panic: 测试错误处理
- ignore: 跳过慢测试
术语表
| English | 中文 |
|---|---|
| Unit Test | 单元测试 |
| Integration Test | 集成测试 |
| Doc Test | 文档测试 |
| Assertion | 断言 |
| Panic | 恐慌 |
| Test Fixture | 测试夹具 |
完整示例:src/advance/testing/test_sample.rs
知识检查
快速测验(答案在下方):
-
#[cfg(test)]的作用是什么? -
assert!、assert_eq!、assert_ne!的区别? -
如何测试会 panic 的函数?
点击查看答案与解析
- 只在测试编译时包含代码
assert!= 条件为真,assert_eq!= 相等,assert_ne!= 不相等- 使用
#[should_panic]属性
关键理解: 测试是代码质量的重要保障。
继续学习
💡 记住:好的测试是代码最好的文档!
Mock 模拟测试
开篇故事
想象你要测试一个依赖数据库的服务。传统方式是:连接真实数据库 → 插入测试数据 → 测试 → 清理数据。Mock 就像是假数据库——它模拟数据库的行为,但不需要真实连接。mockall 库帮你轻松创建这些"假"对象。
本章适合谁
如果你需要编写单元测试(测试依赖外部服务、数据库、API),本章适合你。Mock 是单元测试的关键技术。
你会学到什么
完成本章后,你可以:
- 理解 Mock 测试概念
- 使用 mockall 创建 Mock 对象
- 模拟 trait 实现
- 设置期望和返回值
- 验证方法调用
前置要求
- 测试基础 - 测试基础
- 特征 - 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
}
});
小结
核心要点:
- mockall: 自动生成 Mock
- #[automock]: 为 trait 生成 Mock
- expect_*(): 设置期望
- returning(): 设置返回值
- times(): 验证调用次数
关键术语:
- Mock: 模拟对象
- Expectation: 期望
- Predicate: 谓词匹配
- Stub: 桩实现
术语表
| English | 中文 |
|---|---|
| Mock | 模拟对象 |
| Expectation | 期望 |
| Predicate | 谓词 |
| Stub | 桩 |
| Trait | 特征 |
知识检查
快速测验(答案在下方):
-
#[automock]属性做了什么? -
如何设置 Mock 的返回值?
-
Mock 和真实实现的区别?
点击查看答案与解析
- 自动生成 trait 的 Mock 实现(
MockTraitName) mock.expect_method().returning(|args| value)- Mock 是测试用的假实现,可控行为;真实实现是生产用的
关键理解: Mock 让你隔离测试,不依赖外部系统。
继续学习
前一章: Cow 类型
下一章: 测试框架
相关章节:
- Cow 类型
- 测试框架
- 特征
返回: 高级进阶
完整示例: mock_sample.rs
RSpec 测试框架
开篇故事
想象你要写测试报告。传统方式是:写测试函数 → 断言 → 打印结果。RSpec 就像是:用自然语言描述测试——"describe 用户服务,it 应该创建用户,it 应该删除用户"。rspec crate 帮你用 BDD 风格写测试。
本章适合谁
如果你想用 BDD 风格编写测试(行为驱动开发),本章适合你。RSpec 让测试更像文档。
你会学到什么
完成本章后,你可以:
- 理解 BDD 测试概念
- 使用 rspec crate
- 使用 speculate 宏
- 编写描述性测试
- 组织测试套件
前置要求
依赖安装
运行以下命令安装所需依赖:
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 特性时运行
}
}
}
小结
核心要点:
- rspec: BDD 测试框架
- describe: 组织测试
- it: 单个测试
- before: 共享上下文
- speculate!: 宏语法
关键术语:
- BDD: 行为驱动开发
- Describe: 描述块
- It: 测试用例
- Before: 前置块
术语表
| English | 中文 |
|---|---|
| BDD | 行为驱动开发 |
| Describe | 描述块 |
| It | 测试用例 |
| Before | 前置块 |
| Speculate | 推测宏 |
知识检查
快速测验(答案在下方):
-
RSpec 风格测试和普通 Rust 测试有什么区别?
-
describe和it的作用是什么? -
什么时候应该使用 BDD 风格测试?
点击查看答案与解析
- RSpec 使用
describe/it语法,更接近自然语言 describe= 测试组,it= 单个测试用例- 复杂业务逻辑、行为驱动开发、团队协作
关键理解: BDD 测试更易读,但需要额外依赖。
继续学习
相关章节:
返回: 高级进阶
完整示例: rspec_sample.rs
派生宏
开篇故事
想象你要为结构体实现 getter 和 setter 方法。传统方式是:手动写每个方法 → 容易出错 → 代码重复。派生宏就像是:告诉编译器"帮我生成这些方法",它自动完成。getset crate 就是这样的工具。
本章适合谁
如果你想减少样板代码(getter、setter、builder),本章适合你。派生宏是 Rust 元编程的基础。
你会学到什么
完成本章后,你可以:
- 理解派生宏概念
- 使用 getset crate
- 自动生成 getter/setter
- 使用 derive_more
- 创建 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,
}
小结
核心要点:
- getset: 自动生成 getter/setter
- derive_more: 派生各种 trait
- Builder: 链式构建对象
- 可见性: 控制访问级别
- 减少样板: 提高开发效率
关键术语:
- Getter: 获取字段值
- Setter: 设置字段值
- Builder: 构建器模式
- Derive: 派生宏
术语表
| English | 中文 |
|---|---|
| Getter | 获取方法 |
| Setter | 设置方法 |
| Builder | 构建器 |
| Derive | 派生 |
| Macro | 宏 |
知识检查
快速测验(答案在下方):
-
#[derive(Getters)]生成什么代码? -
#[get = "pub"]和#[get]有什么区别? -
什么时候应该使用派生宏而不是手动实现?
点击查看答案与解析
- 为每个字段生成
fn field_name(&self) -> &Type方法 #[get = "pub"]生成公共方法,#[get]生成私有方法- 字段多、样板代码多时使用派生宏,减少重复
关键理解: 派生宏是减少样板代码的有效工具。
延伸阅读
学习完派生宏后,你可能还想了解:
- derive_more crate - 更多派生宏
- smart-default crate - 默认值派生
- typed-builder crate - Builder 模式派生
选择建议:
继续学习
前一章: RSpec 测试框架
下一章: 宏编程
相关章节:
- RSpec 测试框架
- 宏编程
- 结构体
返回: 高级进阶
完整示例: getset_sample.rs
宏编程
开篇故事
想象你经常写重复的代码。传统方式是:复制粘贴 → 修改 → 容易出错。宏就像是:告诉编译器"按这个模板生成代码",它自动完成。Rust 宏是强大的元编程工具。
本章适合谁
如果你想减少代码重复、创建 DSL(领域特定语言),本章适合你。宏是 Rust 元编程的基础。
你会学到什么
完成本章后,你可以:
- 理解宏的概念
- 使用声明宏 (macro_rules!)
- 使用过程宏 (proc-macro)
- 创建自定义宏
- 理解宏卫生
前置要求
- 函数 - 函数基础
- 特征 - 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
小结
核心要点:
- macro_rules!: 声明宏
- proc-macro: 过程宏
- 卫生: 宏作用域隔离
- 元编程: 编译时生成代码
关键术语:
- Macro: 宏
- macro_rules!: 声明宏
- proc-macro: 过程宏
- Hygiene: 卫生
- DSL: 领域特定语言
术语表
| English | 中文 |
|---|---|
| Macro | 宏 |
| macro_rules! | 声明宏 |
| Procedural Macro | 过程宏 |
| Hygiene | 卫生 |
| DSL | 领域特定语言 |
知识检查
快速测验(答案在下方):
-
声明宏和过程宏有什么区别?
-
macro_rules!中的$()语法是什么? -
过程宏需要什么类型的 crate?
点击查看答案与解析
- 声明宏 = 模式匹配替换,过程宏 = Rust 代码操作 AST
- 重复匹配:
$(...),*匹配逗号分隔的零或多个项 proc-macro = true的 crate 类型
关键理解: 声明宏适合简单代码生成,过程宏适合复杂转换。
继续学习
相关章节:
- 派生宏
- 特征
- 模块
返回: 高级进阶
完整示例: macros_sample.rs
类型别名
开篇故事
想象你经常写很长的类型名:Arc<RefCell<HashMap<String, Vec<User>>>>。每次都写很繁琐。类型别名就像是:给长类型起个短名字——type UserData = Arc<...>。这样代码更清晰易读。
本章适合谁
如果你想简化复杂类型、提高代码可读性,本章适合你。类型别名是 Rust 代码组织的基础。
你会学到什么
完成本章后,你可以:
- 理解类型别名概念
- 创建类型别名
- 简化复杂类型
- 使用类型别名组织代码
- 实现双向链表
前置要求
- 智能指针 - 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;
}
小结
核心要点:
- type: 创建类型别名
- 简化: 简化复杂类型
- 可读性: 提高代码可读性
- 零成本: 无性能开销
- 组织: 组织代码结构
关键术语:
- Type Alias: 类型别名
- Arc: 原子引用计数
- Weak: 弱引用
- RefCell: 运行时借用检查
术语表
| English | 中文 |
|---|---|
| Type Alias | 类型别名 |
| Arc | 原子引用计数 |
| Weak | 弱引用 |
| RefCell | 引用单元 |
| Bidirectional List | 双向链表 |
知识检查
快速测验(答案在下方):
-
type和newtype有什么区别? -
类型别名会增加运行时开销吗?
-
什么时候应该使用类型别名?
点击查看答案与解析
type只是别名(编译时),newtype是新类型(包装结构体)- 不会 - 类型别名在编译时消除
- 简化复杂类型、提高可读性、减少重复
关键理解: 类型别名是零成本抽象。
继续学习
前一章: 宏编程
下一章: 对象存储
相关章节:
- 宏编程
- 智能指针
- 泛型
返回: 高级进阶
完整示例: typealias_sample.rs
对象存储
开篇故事
想象你要存储大量文件(图片、文档、备份)。传统方式是:买硬盘 → 管理文件系统 → 处理备份。对象存储就像是:云存储仓库——你只管存储和读取,其他都交给服务处理。
本章适合谁
如果你需要在 Rust 程序中存储和检索文件(本地或云存储),本章适合你。object_store 是统一的对象存储接口。
你会学到什么
完成本章后,你可以:
- 理解对象存储概念
- 使用 LocalFileSystem
- 存储键值对
- 检索对象数据
- 列出对象列表
前置要求
- 异步编程 - 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);
小结
核心要点:
- ObjectStore: 统一接口
- LocalFileSystem: 本地存储
- put(): 存储对象
- get(): 检索对象
- list(): 列出对象
关键术语:
- Object Store: 对象存储
- Path: 路径
- Put: 存储
- Get: 获取
术语表
| English | 中文 |
|---|---|
| Object Store | 对象存储 |
| Path | 路径 |
| Put | 存储 |
| Get | 获取 |
| List | 列出 |
知识检查
快速测验(答案在下方):
-
对象存储和文件系统有什么区别?
-
常见的对象存储服务有哪些?
-
object_storecrate 提供什么抽象?
点击查看答案与解析
- 对象存储是扁平的(key-value),文件系统是分层的(目录树)
- AWS S3, GCP Cloud Storage, Azure Blob Storage
- 统一的对象存储接口,支持多种后端
关键理解: 对象存储适合大规模数据存储和访问。
继续学习
前一章: 资源嵌入
下一章: Ollama AI 集成
相关章节:
- 资源嵌入
- Ollama AI 集成
- 异步编程
返回: 高级进阶
完整示例: objectstore_sample.rs
服务框架
开篇故事
想象你在经营餐厅。单个厨师可以炒菜,但要有秩序地运营餐厅,你需要:前台接待、后厨管理、传菜员、收银员。服务框架就像餐厅管理系统——协调各个"服务"(组件)有序工作,处理顾客(请求)订单。
在 Rust 中,服务框架提供应用生命周期管理、依赖注入、错误处理等基础设施,让你专注于业务逻辑而非样板代码。
本章适合谁
如果你要构建中型到大型的 Rust 应用(Web 服务、微服务、后台进程),本章适合你。服务框架帮助你组织代码、管理依赖、处理错误。
你会学到什么
完成本章后,你可以:
- 理解服务框架的核心组件
- 实现依赖注入模式
- 管理服务生命周期
- 设计可测试的服务架构
前置要求
学习本章前,你需要理解:
依赖安装
运行以下命令安装所需依赖:
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
}
}
发生了什么?
Servicetrait 定义服务接口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![]) // 返回测试数据
}
}
小结
核心要点:
- 服务接口: 定义统一的 start/stop/health 方法
- 依赖注入: 通过 trait 解耦服务
- 生命周期: 管理服务的启动和停止
- 健康检查: 提供服务状态监控
关键术语:
- 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
知识检查
快速测验(答案在下方):
-
服务生命周期的三个阶段是什么?
-
依赖注入的作用是什么?
-
如何测试使用依赖注入的服务?
点击查看答案与解析
- 启动 (Start) → 运行 (Running) → 停止 (Stop)
- 解耦服务,使组件可替换、可测试
- 注入 Mock 依赖,验证服务行为
关键理解: 好的服务框架让复杂应用变得简单。
继续学习
💡 记住:好的服务框架让复杂应用变得简单。定义清晰的接口,注入依赖,管理生命周期!
阶段复习:高级进阶
开篇故事
想象你学完了所有驾驶技巧——高速公路、夜间驾驶、雨雪天气、紧急避让。现在你需要一次综合路考,把所有技能串联起来。阶段复习就是你的"高级路考"——把分散的高级知识整合成完整的生产能力。
本章适合谁
如果你已经完成了高级进阶部分(异步、数据库、Web、系统编程等),现在想检验自己的学习成果,本章适合你。
你会学到什么
完成本章后,你可以:
- 综合运用异步编程、数据库、Web 框架知识
- 设计包含服务层、数据层、API 层的完整应用
- 识别和修复高级 Rust 编译错误
- 理解生产级 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 是惰性的,不调用 .await 或 block_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 threads | Future 包含非 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 派生和字段类型 |
小结
核心要点:
- 异步编程是生产级 Rust 的基础
- 数据库 + Web 是最常见的应用组合
- 错误处理必须贯穿整个应用
- 测试保证代码质量
- 复习是巩固知识的关键
关键术语:
- Future: 异步计算
- Executor: 执行器 (Tokio)
- ORM: 对象关系映射
- REST API: RESTful 接口
- Mock: 模拟对象
术语表
| English | 中文 |
|---|---|
| Future | 异步计算 |
| Executor | 执行器 |
| ORM | 对象关系映射 |
| REST API | RESTful 接口 |
| Mock | 模拟对象 |
| Serialization | 序列化 |
| Deserialization | 反序列化 |
继续学习
💡 记住:复习是学习的重要部分。不要急于前进,确保每个概念都理解了!
💡 记住:高级概念需要更多实践。不要只看代码,动手写、运行、调试!
精选实战 (Awesome)
📖 学习内容概览
太棒了!你已经完成了 基础入门 和 高级进阶!现在你已经掌握了 Rust 的核心概念和生态系统工具。在精选实战部分,你将学习如何构建生产级的应用程序,使用真实的框架和最佳实践。
🎯 你将学到什么
完成本部分学习后,你将能够:
- 构建生产服务 - 使用服务框架和生命周期管理
- 依赖注入 - 实现松耦合的模块化架构
- 消息队列集成 - 使用 MQTT 进行异步通信
- 模板引擎 - 使用 Tera、Liquid、Pest 生成动态内容
- 数据处理 - 使用 Polars 进行数据分析
- 插件系统 - 构建可扩展的插件架构
📚 章节列表
| 章节 | 说明 | 难度 | 预计时间 |
|---|---|---|---|
| 服务框架 | 服务生命周期、注册、管理 | 🔴 困难 | 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)
建议具备:
- 微服务架构基础概念
- 消息队列基础概念
- 设计模式基础(尤其是依赖注入)
📈 学习路径
高级进阶完成
↓
服务框架 → 依赖注入
↓
数据库实战 → 序列生成 → 数据处理
↓
消息队列 → 模板引擎
↓
插件系统
↓
毕业项目
推荐学习顺序:
-
架构基础 (必须先学):
- 服务框架 → 依赖注入
-
数据层 (核心技能):
- 数据库实战 → 序列生成 → 数据处理
-
通信层 (进阶):
- 消息队列 → 模板引擎
-
扩展能力 (深入):
- 插件系统
✅ 学习检查点
完成本部分后,你应该能够:
- 设计服务生命周期管理
- 实现依赖注入容器
- 使用 SurrealDB 进行文档数据库操作
- 集成 MQTT 消息队列
- 生成唯一标识符 (UUID、雪花)
- 使用 Tera/Liquid 模板引擎
- 使用 Polars 进行数据分析
- 设计和实现插件系统
- 构建生产级 Rust 应用
🎓 实践项目
毕业项目建议:
-
微服务架构:
- 使用服务框架构建多个微服务
- 通过消息队列通信
- 使用依赖注入管理依赖
-
数据驱动应用:
- 使用 SurrealDB 存储数据
- 使用 Polars 分析数据
- 使用模板引擎生成报告
-
可扩展平台:
- 实现插件系统
- 支持动态加载插件
- 提供服务注册和发现
📖 完整示例代码
本部分所有示例代码都来自真实项目:
| 样例 | GitHub 链接 |
|---|---|
| Tera 模板 | tera_sample.rs |
| Liquid 模板 | liquid_sample.rs |
| Pest 解析器 | pest_sample.rs |
🏆 毕业认证
完成所有精选实战章节后,你将获得:
- ✅ Rust 高级编程能力
- ✅ 生产级应用架构经验
- ✅ 真实项目代码样例
- ✅ 完整的作品集项目
➡️ 下一步
完成精选实战后,你可以:
-
继续深造:
- 算法实现 - 链表、PI 计算
- LeetCode 题解 - 面试准备
-
实战项目:
- 构建完整的 Web 应用
- 创建开源 Rust 库
- 贡献 Rust 社区
-
职业发展:
- Rust 后端工程师
- 系统程序员
- 区块链开发
准备好了吗?让我们开始 服务框架 的学习! 🚀
数据库高级应用
开篇故事
想象你正在开发一个智能推荐系统,需要存储数百万用户的行为数据,并快速找到相似用户。传统关系型数据库擅长结构化查询,但在处理向量相似度搜索时力不从心。现代应用需要多模态数据库——既能处理传统表格数据,又能进行高效的向量检索。本章介绍两种强大的 Rust 数据库方案:SurrealDB(多模型云原生数据库)和 SQLite + sqlite-vec(轻量级向量扩展),让你的应用具备 AI 时代的核心竞争力。
本章适合谁
如果你已经掌握了 SQLx 或 Diesel 等传统 ORM,现在想探索更先进的数据库技术,本章适合你。无论你是要构建 AI 应用、处理复杂数据关系,还是需要轻量级嵌入式向量搜索,这里都有适合你的方案。
你会学到什么
完成本章后,你可以:
- 使用 SurrealDB 进行文档存储和关系查询
- 理解 SurrealDB 的 RecordID 和命名空间概念
- 集成 sqlite-vec 扩展进行向量相似度搜索
- 将向量数据库用于推荐系统和语义搜索
- 在多模型数据库中管理复杂数据关系
- 选择合适的嵌入式数据库方案
前置要求
学习本章前,你需要理解:
- 异步编程 -
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,
}
发生了什么?
Surreal::new::<Mem>(())- 创建内存数据库(无需安装服务器)use_ns("test").use_db("test")- 选择命名空间和数据库create("person")- 在 person 表中创建记录.content()- 使用 Serde 序列化 Rust 结构体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 生成方式对比
| 方式 | 语法 | 特点 | 适用场景 |
|---|---|---|---|
| 指定 ID | person:jaime | 可预测、可读 | 用户 ID、固定配置 |
| ULID | person:ulid() | 时间排序、唯一 | 日志、事件 |
| UUID | person: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
解析:
sqlite_version()返回 SQLite 版本号vec_version()返回 sqlite-vec 扩展版本- 两个函数都在 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 - 100k | sqlite-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: 几种优化策略:
- 维度降维:使用 PCA 将高维向量降至 128-512 维
- 量化:使用 int8 替代 float32,减少 75% 存储
- 分区:按类别分区,缩小搜索空间
- 缓存:缓存热门查询结果
知识扩展 (选学)
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)?;
小结
核心要点:
- SurrealDB 是多模型数据库,支持文档、图、关系模型
- RecordId 是 SurrealDB 的核心概念,支持多种 ID 生成策略
- sqlite-vec 为 SQLite 添加轻量级向量搜索能力
- 向量搜索使用距离算法(欧几里得、余弦)找到相似项
- 选择数据库需考虑数据规模、查询模式和部署环境
关键术语:
- RecordId: SurrealDB 记录唯一标识符
- Namespace/Database: SurrealDB 的命名空间层级
- 向量数据库 (Vector DB): 支持相似度搜索的数据库
- Embedding: 文本/图像的向量表示
- Similarity Search: 基于向量距离的内容检索
下一步:
- 探索 SurrealDB 官方文档
- 学习 sqlite-vec 项目
- 实践 向量搜索应用
术语表
| 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 适合对象存储服务。
参考资料
记住:现代应用往往需要多种数据模型。SurrealDB 提供一站式解决方案,而 sqlite-vec 为轻量级应用带来向量能力。选择适合你场景的工具,而不是试图用一个方案解决所有问题。
服务框架与微服务架构
开篇故事
想象一下,你正在经营一家大型连锁餐厅。最初只有一家店,你自己就能管理所有的事情——采购、烹饪、服务、收银。但随着业务扩大,你开了十家店、一百家店,你不可能亲自管理每一家店的每个环节。
于是你设计了这样的架构:
- 中央厨房(服务注册中心)统一协调所有分店的食材供应
- 各分店(微服务)专注于自己的业务:有的专做寿司、有的专做披萨、有的专做甜品
- 服务员(gRPC 客户端)知道如何找到对应的分店下单
- 经理(服务框架)负责监控每个分店的运营状态,在出现问题时及时调配资源
在软件世界里,这就是微服务架构——将大型应用拆分为多个独立部署、独立扩展的小型服务。awesome crate 中的服务框架就是一套生产级的"餐厅管理系统",它包含了服务发现(Consul)、远程调用(gRPC)、依赖注入(DI)、生命周期管理等核心组件。
本章适合谁
如果你已经掌握了 Rust 基础和异步编程,现在希望构建:
- 生产级的微服务应用
- 支持服务注册与发现的服务集群
- 基于 gRPC 的高性能 RPC 服务
- 具备依赖注入和插件机制的可扩展系统
本章适合你。微服务架构是现代分布式系统的主流选择,而这些知识将帮助你构建企业级应用。
你会学到什么
完成本章后,你可以:
- 理解依赖注入(DI)的三种实现方式:具体类型注入、动态 trait 注入(Arc)、动态 trait 注入(Box)
- 使用
inventorycrate 实现编译时插件注册 - 使用 Consul 进行服务注册、发现和配置管理
- 使用 Tonic 编写 gRPC 服务端和客户端
- 实现自定义服务生命周期框架,支持优雅启动和关闭
- 构建支持流式数据传输的 gRPC 服务
- 理解微服务架构中的健康检查和服务治理
前置要求
学习本章前,你需要理解:
- Tokio 异步运行时 - 特别是
spawn、await、通道的使用 - 所有权与生命周期 - 特别是
Arc、trait 对象的理解 - 泛型与 trait - 理解泛型约束和 trait 对象
- 线程与并发 - 了解并发基本概念
本章涉及的 crate:
tonic- gRPC 框架consul/ 自定义 HTTP 客户端 - 服务注册与发现inventory- 编译时插件注册async-trait- 异步 traitserde- 配置序列化
依赖安装
运行以下命令安装所需依赖:
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(®istration_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(®istration).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: 检查以下几点:
- 服务名称是否完全匹配(大小写敏感)
- Consul 的健康检查是否通过(失败的服务会被过滤)
- 是否等待了足够的时间(服务注册有延迟)
// 调试技巧:打印所有已注册服务
let services = consul.get_all_registered_service_names(None).await?;
println!("Registered services: {:?}", services);
Q: gRPC 客户端连接失败?
A: 常见原因:
- 地址格式:必须使用
http://前缀 - 端口错误:检查服务端的监听端口
- 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");
}
}
}
小结
核心要点:
- 依赖注入有三种模式:具体类型注入(零开销)、动态 Arc 注入(灵活共享)、动态 Box 注入(独占所有权)
- Service Container 使用
TypeId进行类型安全的运行时服务查找 - Consul 提供服务注册、发现、健康检查和配置管理
- Tonic 是 Rust 生态中功能完整的 gRPC 框架,支持 unary、streaming、拦截器等
- ApplicationFramework 通过
RunnableServicetrait 统一管理服务生命周期 - 优雅关闭使用
oneshot::channel传递信号,确保资源正确释放
关键术语:
| English | 中文 |
|---|---|
| Dependency Injection (DI) | 依赖注入 |
| Service Registry | 服务注册中心 |
| Service Discovery | 服务发现 |
| Health Check | 健康检查 |
| gRPC | gRPC 远程过程调用 |
| 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 Object | Trait 对象 |
| Service Registry | 服务注册表 |
| Service Discovery | 服务发现 |
| Consul | Consul 服务治理工具 |
| gRPC | Google RPC 框架 |
| Tonic | Rust gRPC 实现 |
| Protobuf | Protocol 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>()是具体类型的 TypeIdTypeId::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 都正确
解析:
- 空列表问题:如果 Consul 返回空列表,
nodes[0]会导致 panic,应该使用nodes.get(0)或检查is_empty() - 负载均衡:生产环境应该使用轮询(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 都正确
解析:
- panic 处理:如果
self.server.serve()panic,注销代码不会执行,导致"僵尸服务"残留在 Consul - 缺少 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(())
}
扩展阅读
官方资源
相关项目
进阶主题
- Service Mesh: Istio、Linkerd 等服务网格技术
- gRPC 流控: 背压(backpressure)和流控策略
- 分布式追踪: OpenTelemetry、Jaeger 集成
- 服务熔断: 使用
tower::limit实现熔断和限流
继续学习
- 下一步:Tokio 异步运行时
- 进阶:Axum Web 框架
- 相关:数据库访问
💡 记住:微服务架构的核心是解耦和自治——每个服务独立部署、独立扩展,通过标准协议(gRPC/HTTP)通信。服务框架让这些理念在 Rust 中变得可实现!
依赖注入 (Dependency Injection)
开篇故事
想象你在组装一台电脑。如果你把 CPU、内存、硬盘全部焊死在主板上(硬编码依赖),那么升级任何一个部件都需要更换整块主板。但如果你使用插槽和接口(依赖注入),你可以随时更换任何部件,而无需改动主板。
在软件中,依赖注入就是这种"插槽"机制——服务不自己创建依赖,而是通过外部"注入"依赖。这让代码更易测试、更易维护、更易扩展。
本章适合谁
如果你在构建中大型 Rust 应用,需要管理服务之间的依赖关系、实现可测试的代码架构、或者理解 Rust 中的 DI 模式,本章适合你。
你会学到什么
完成本章后,你可以:
- 理解 Rust 中依赖注入的三种主要模式
- 使用具体类型注入(Concrete Injection)
- 使用 Arc 和 Box 实现动态依赖注入
- 构建 Service Container(服务容器)
- 使用工厂模式实现延迟初始化
- 解析具体服务和 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: 工厂模式的优势是什么?
点击查看答案与解析
- 具体类型注入(泛型)、Trait 对象注入(Arc/Box)、服务容器(TypeId)
- Arc 支持共享所有权(多服务共享),Box 独占所有权(单服务使用)
- 延迟初始化、自动解析依赖链、缓存结果
关键理解: Rust 的 DI 更注重显式和类型安全,而非魔法。
延伸阅读
学习完依赖注入后,你可能还想了解:
- 服务生命周期管理 - ApplicationFramework 模式
- 插件系统 - 编译时插件注册
- shaku crate - 类型安全的 DI 框架
- inject crate - 编译时 DI
选择建议:
- 简单项目 → 具体类型注入(泛型)
- 中型项目 → Arc 动态注入
- 大型项目 → 服务容器 + 工厂模式
小结
核心要点:具体注入零开销、Arc 支持共享、Service Container 最灵活、工厂模式延迟初始化
完整示例:crates/awesome/src/services/
插件系统 (Plugin System)
开篇故事
想象你在开发一个图像编辑器。最初只支持 JPEG 和 PNG 格式。但随着用户需求增长,你需要支持 WebP、AVIF、HEIC 等新格式。如果每次都要修改核心代码、重新编译整个应用,开发效率会非常低。
插件系统就像给应用预留了"扩展插槽"——新功能可以作为独立模块插入,无需修改核心代码。在 Rust 中,由于编译时类型安全和无运行时反射的特性,实现插件系统需要特殊的设计模式。本章将介绍 Rust 中实现插件系统的多种方案。
本章适合谁
如果你想学习:
- Rust 中如何实现可扩展的插件架构
- 编译时插件注册 vs 运行时动态加载的区别
- 如何设计可插拔的服务架构
本章适合你。插件系统是构建可扩展应用的核心技术。
你会学到什么
完成本章后,你可以:
- 理解 Rust 插件系统的三种主要实现方式
- 使用
inventorycrate 实现编译时插件注册 - 使用
libloadingcrate 实现运行时动态加载 - 使用
dlopencrate 实现 C 兼容的动态库 - 根据场景选择合适的插件方案
- 设计可插拔的服务架构
前置要求
依赖安装
不同插件方案需要不同的依赖:
# 方案 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 }
}
小结
核心要点:
- 编译时插件 (inventory) - 类型安全、零开销,适合内置扩展
- 运行时插件 (libloading) - 热插拔、灵活,需要 unsafe
- WASM 插件 (extism) - 沙箱安全、跨语言,推荐用于第三方插件
- 设计原则 - 定义清晰的 trait 接口、使用注册模式管理插件
关键术语:
| English | 中文 | 说明 |
|---|---|---|
| Plugin | 插件 | 可扩展的功能模块 |
| Registry | 注册表 | 管理所有已注册插件 |
| Hot Reload | 热重载 | 运行时加载/卸载插件 |
| WASM | WebAssembly | 安全的沙箱执行环境 |
| ABI | 应用二进制接口 | 跨语言兼容的接口规范 |
| cdylib | C 动态库 | 编译为 C 兼容的动态库 |
下一步:
术语表
| English | 中文 |
|---|---|
| Plugin System | 插件系统 |
| Registry | 注册表 |
| Hot Reload | 热重载 |
| Dynamic Library | 动态库 |
| Symbol | 符号 |
| ABI | 应用二进制接口 |
| WASM | WebAssembly |
| Sandbox | 沙箱 |
| cdylib | C 动态库 |
| rlib | Rust 静态库 |
完整示例:crates/awesome/src/services/inventory_sample.rs
继续学习
💡 记住:Rust 的插件系统设计核心是"编译时安全 + 运行时灵活"。根据需求选择 inventory(编译时)、libloading(运行时)、或 extism(WASM 沙箱)!
序列 ID 生成
开篇故事
想象你在设计一个电商平台。每当有新订单产生,系统需要为订单分配一个唯一的标识符。如果两个订单获得相同的 ID,后果可能是客户 A 支付了客户 B 的订单、库存系统混乱、财务报表数据错误。
在分布式系统中,这个问题更加棘手。多个服务器同时处理请求,如何保证生成的 ID 在全局范围内唯一?这就是 UUID (Universally Unique Identifier) 的价值所在。
本章适合谁
如果你需要为数据库记录生成唯一主键、在分布式系统中标识资源、实现订单号或流水号等业务 ID,本章适合你。
你会学到什么
完成本章后,你可以:
- 使用
uuidcrate 生成不同版本的 UUID (v3, v4, v5, v7) - 根据业务场景选择合适的 UUID 版本
- 基于业务字段生成确定性的 GUID
- 解析和格式化 UUID 字符串
- 使用 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 地址 | 可排序,暴露硬件 | 遗留系统兼容 |
| v3 | MD5 哈希 | 确定性,已弃用 | 旧系统兼容 |
| v4 | 随机数 | 最常用,简单 | 通用唯一标识 |
| v5 | SHA-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 的区别是什么?
点击查看答案与解析
- UUID v4 使用加密安全的随机数生成器
- UUID v7(时间排序,索引友好,避免页分裂)
- 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
本章适合你。消息队列是构建可扩展、松耦合系统的关键技术。
你会学到什么
完成本章后,你可以:
- 解释消息队列的核心概念和应用场景
- 理解 MQTT 协议的发布/订阅模式
- 使用 rumqttc 创建同步和异步 MQTT 客户端
- 实现消息的发布(Publish)和订阅(Subscribe)
- 理解 QoS 等级及其对消息可靠性的影响
- 编写基于 MQTT 的物联网通信程序
- 处理连接保持和重连逻辑
前置要求
学习本章前,你需要理解:
- 所有权 - 理解所有权和生命周期
- 异步编程 - 理解 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);
}
}
发生了什么?
MqttOptions配置 MQTT Broker 地址和客户端 IDClient::new()创建同步客户端,返回(client, connection)元组subscribe()订阅指定主题,准备接收消息publish()向主题发送消息,所有订阅者都会收到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 等级 | 名称 | 说明 | 应用场景 |
|---|---|---|---|
| 0 | AtMostOnce | 最多一次,不确认 | 高频数据,丢失可接受 |
| 1 | AtLeastOnce | 至少一次,需确认 | 关键数据,允许重复 |
| 2 | ExactlyOnce | 恰好一次,四次握手 | 关键命令,不可重复 |
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);
}
}
点击查看答案与解析
问题:
- 没有设置
keep_alive,连接可能被 Broker 断开 connection.iter()返回Result,需要正确处理- 发送和接收在同一线程,没有并发处理
修复方案:
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 | 可能丢失? | 原因 |
|---|---|---|---|
| A | 0 | ✅ 可能丢失 | 发送即忘,无确认 |
| B | 1 | ❌ 不会丢失 | 需要 ACK,会重试 |
| C | 2 | ❌ 不会丢失 | 四次握手确保送达 |
关键点:
- 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: 检查以下几点:
-
Broker 是否运行:
# 测试 Broker 连通性 mosquitto_pub -t test -m "hello" -h 127.0.0.1 -
端口是否正确:
- MQTT 默认端口:1883
- MQTT over TLS:8883
- WebSocket:8083
-
防火墙设置:
# 开放端口(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:
| 特性 | MQTT | Kafka | RabbitMQ |
|---|---|---|---|
| 协议复杂度 | 简单 | 中等 | 复杂 |
| 适用场景 | 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
));
小结
核心要点:
- 消息队列 实现了生产者与消费者的解耦
- MQTT 是轻量级的发布/订阅协议,适合 IoT
- Topic 使用层级结构组织消息,
+和#是通配符 - QoS 控制消息可靠性:0(最快) → 2(最可靠)
- 同步客户端 使用
Client,异步客户端 使用AsyncClient - 遗嘱消息 在意外断开时自动通知其他客户端
关键术语:
| 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
- 实现物联网设备间的消息通信
本章适合你。
你会学到什么
完成本章后,你可以:
- 理解 MQTT 的核心概念(Broker、Topic、QoS)
- 使用 rumqttc 创建同步和异步 MQTT 客户端
- 实现消息的发布(Publish)和订阅(Subscribe)
- 理解 QoS 等级及其对消息可靠性的影响
- 处理连接保持和重连逻辑
- 编写基于 MQTT 的物联网通信程序
前置要求
学习本章前,你需要理解:
安装 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);
}
}
发生了什么?
MqttOptions配置 MQTT Broker 地址和客户端 IDClient::new()创建同步客户端,返回(client, connection)元组subscribe()订阅指定主题,准备接收消息publish()向主题发送消息,所有订阅者都会收到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 等级 | 名称 | 说明 | 应用场景 |
|---|---|---|---|
| 0 | AtMostOnce | 最多一次,不确认 | 高频数据,丢失可接受 |
| 1 | AtLeastOnce | 至少一次,需确认 | 关键数据,允许重复 |
| 2 | ExactlyOnce | 恰好一次,四次握手 | 关键命令,不可重复 |
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);
}
}
点击查看答案与解析
问题:
- 没有设置
keep_alive,连接可能被 Broker 断开 connection.iter()返回Result,需要正确处理- 发送和接收在同一线程,没有并发处理
修复方案:
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 | 可能丢失? | 原因 |
|---|---|---|---|
| A | 0 | ✅ 可能丢失 | 发送即忘,无确认 |
| B | 1 | ❌ 不会丢失 | 需要 ACK,会重试 |
| C | 2 | ❌ 不会丢失 | 四次握手确保送达 |
关键点:
- 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: 检查以下几点:
-
Broker 是否运行:
# 测试 Broker 连通性 mosquitto_pub -t test -m "hello" -h 127.0.0.1 -
端口是否正确:
- MQTT 默认端口:1883
- MQTT over TLS:8883
- WebSocket:8083
-
防火墙设置:
# 开放端口(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:
| 特性 | MQTT | Kafka | RabbitMQ |
|---|---|---|---|
| 协议复杂度 | 简单 | 中等 | 复杂 |
| 适用场景 | 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
));
小结
核心要点:
- MQTT 是轻量级的发布/订阅协议,适合 IoT
- Topic 使用层级结构组织消息,
+和#是通配符 - QoS 控制消息可靠性:0(最快) → 2(最可靠)
- 同步客户端 使用
Client,异步客户端 使用AsyncClient - 遗嘱消息 在意外断开时自动通知其他客户端
关键术语:
| English | 中文 | 说明 |
|---|---|---|
| MQTT | 消息队列遥测传输 | 轻量级消息协议 |
| Broker | 消息代理 | 消息中转服务器 |
| Topic | 主题 | 消息的地址标识 |
| Publish | 发布 | 发送消息到主题 |
| Subscribe | 订阅 | 注册接收主题消息 |
| QoS | 服务质量 | 消息可靠性等级 |
| Payload | 消息载荷 | 实际传输的数据 |
| Retained | 保留消息 | 存储在 Broker 的最后一条消息 |
| LastWill | 遗嘱消息 | 意外断开时自动发送的消息 |
下一步:
术语表
| 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,本章适合你。
你会学到什么
完成本章后,你可以:
- 使用 Tera 模板引擎渲染 HTML(循环、条件、过滤器)
- 使用 Liquid 模板生成安全的内容(内联 + 文件)
- 使用 Pest 解析器构建自定义语法(PEG 语法)
- 根据场景选择合适的模板引擎
前置要求
- 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
原理解析
模板引擎对比
| 引擎 | 语法 | 适用场景 | 安全性 |
|---|---|---|---|
| Tera | Jinja2-like | HTML 页面、Web 应用 | 中等 |
| Liquid | Shopify Liquid | 静态网站、用户自定义模板 | 高(沙箱) |
| Pest | PEG 解析器 | 自定义 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 适合什么场景?
点击查看答案与解析
- Tera(Jinja2 语法,Web 友好,支持结构体序列化)
- 安全性强,沙箱环境,适合用户自定义模板(如 CMS)
- 自定义 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 分钟规则
- 先自己写代码,让编译器报错
- 仔细阅读错误信息(Rust 编译器是最好的老师)
- 如果卡住超过 15 分钟 → 查看答案
- 关掉答案,从头自己写一遍
间隔重复
- 学完一章后,第二天复习 前一章的关键概念
- 每学完 5 章,做一次 综合复习(见复习章节)
- 使用知识检查题测试自己的记忆
主动回忆
- 不要只是阅读代码,自己写一遍
- 合上教程,尝试凭记忆写出关键概念
- 使用"费曼技巧":尝试向别人解释这个概念
最佳实践
- 边学边练 - 每章都要动手练习
- 做笔记 - 记录难点和收获
- 提问 - 在 RustCn 社区提问
- 复习 - 学完一章后复习前一章
- 编译器是你的老师 - 学会阅读错误信息
遇到困难时
- 回到前一章巩固基础
- 看代码示例(每个章节都有)
- 在 community 提问
- 休息后再试
- 记住:感到困惑是完全正常的! 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
- 异步服务实现
动手试试:
- 添加一个新的 RPC 方法
- 修改返回格式
- 添加日志输出
相关章节:
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 行/题
难度: 🟢 入门
题目列表:
运行:
cd crates/leetcode
cargo test
学习重点:
- 数据结构应用
- 算法实现
- 测试驱动
相关章节:
框架实战
8. Awesome 框架应用 🔴
目录: crates/awesome/src/
代码量: ~2000 行
难度: 🔴 高级
包含:
- 服务生命周期管理
- 依赖注入
- 数据库连接池
- gRPC 服务
学习重点:
- 生产级架构
- 设计模式
- 错误处理最佳实践
相关章节:
项目完成清单
基础阶段
-
- Hello Rust 基础演示
-
- 运行所有示例
进阶阶段
-
- gRPC 服务器
-
- gRPC 客户端
-
- UDS IPC
-
- Stdio IPC
算法阶段
-
- PI 值计算
-
- LeetCode 两数之和
-
- LeetCode 两数相加
框架阶段
-
- Awesome 框架概览
-
- 实现自定义服务
-
- 数据库集成实战
学习建议
项目练习流程
- 阅读相关章节 - 先学习理论知识
- 运行示例代码 - 确认环境正常
- 修改代码试验 - 试试改动有什么效果
- 独立完成扩展 - 按练习建议实现功能
遇到问题时
- 查看章节中的"常见错误"
- 搜索错误信息
- 在 RustCN 论坛 提问
- 查看其他项目示例
进阶路径
基础项目 → 进阶项目 → 算法项目 → 框架实战 → 贡献代码
贡献
欢迎贡献更多项目示例!
提交 PR 前确保:
- 代码可编译运行
- 添加相关文档
- 通过测试
- 符合项目风格
下一步: 选择一个项目开始吧!🎯
项目实战:CLI 待办事项管理器
难度: 🟡 中级
代码量: ~170 行
涉及知识点: clap 参数解析、serde 序列化、anyhow 错误处理、文件系统操作
项目目标
构建一个支持增删改查的 CLI 待办事项工具,数据持久化到 JSON 文件。
技术栈
| Crate | 用途 |
|---|---|
clap (derive) | CLI 参数解析 |
serde + serde_json | JSON 数据持久化 |
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 Format | Notes |
|---|---|---|---|
| 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 Format | Notes |
|---|---|---|---|
| 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 | 选项类型 | Option | Type representing optional value |
| Result | 结果类型 | Result<T, E> | Type representing success/error |
| Vector | 向量 | Vec | Growable array |
| HashMap | 哈希映射 | HashMap<K, V> | Key-value map |
| Box | 盒子 | Box | Heap pointer type |
| Rc | 引用计数 | Rc | Reference-counted pointer |
| Arc | 原子引用计数 | Arc | Atomic reference-counted pointer |
特征与泛型 (Traits & Generics)
| English | 中文 | First Use Format | Notes |
|---|---|---|---|
| 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 Format | Notes |
|---|---|---|---|
| Stack | 栈 | 栈 (stack) | LIFO memory region |
| Heap | 堆 | 堆 (heap) | Dynamic memory region |
| Allocation | 分配 | 分配 (allocation) | Memory reservation |
| Deallocation | 释放 | 释放 (deallocation) | Memory return |
| Drop | 丢弃 | Drop trait | Resource 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 Format | Notes |
|---|---|---|---|
| Concurrency | 并发 | 并发 (concurrency) | Multiple tasks in progress |
| Parallelism | 并行 | 并行 (parallelism) | Multiple tasks simultaneous |
| Thread | 线程 | 线程 (thread) | Execution unit |
| Async/Await | 异步/等待 | async/await | Asynchronous programming model |
| Future | 未来值 | Future | Async computation result |
| Executor | 执行器 | 执行器 (executor) | Async task scheduler |
| Runtime | 运行时 | 运行时 (runtime) | Execution environment |
| Tokio | Tokio | Tokio | Async runtime crate |
| Mutex | 互斥锁 | Mutex | Mutual exclusion primitive |
| RwLock | 读写锁 | RwLock | Read-write lock |
| Channel | 通道 | 通道 (channel) | Message passing conduit |
| Send | 发送 | Send trait | Thread-safe transfer trait |
| Sync | 同步 | Sync trait | Thread-shared trait |
| Atomic | 原子操作 | 原子操作 (atomic) | Indivisible operation |
错误处理 (Error Handling)
| English | 中文 | First Use Format | Notes |
|---|---|---|---|
| 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 | 匹配 | match | Pattern 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 Format | Notes |
|---|---|---|---|
| Cargo | Cargo | Cargo | Rust 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 |
| Clippy | Clippy | Clippy | Rust linter tool |
| Rustfmt | Rustfmt | rustfmt | Code 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:
-
First occurrence in chapter: Always use format
中文 (English)- ✅ Correct: "所有权 (ownership) 是 Rust 的核心概念"
- ✅ Correct: "让我们了解 borrowing (借用) 的规则"
- ❌ Wrong: "Ownership 是重要的" (English without translation)
- ❌ Wrong: "所有权是重要的" (No English reference for searchability)
-
Subsequent occurrences: Use Chinese only
- ✅ "所有权系统确保..."
- ✅ "借用规则防止..."
-
Code examples: Keep English keywords
- ✅
let x = 5;(not让 X = 5;) - ✅
fn main()(not函数 主 ())
- ✅
-
Links to external docs: Use English terms for URLs
- ✅ Link to
std::string::String(notstd::字符串::字符串)
- ✅ Link to
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