---
title: 写给 Java 程序员的 Rust 入门
date: 2026-05-09
---

这篇文章终于写完了。一开始没想那么多，写出来发现真的蛮长的，但自己仔细检查过非常多次，应该上手难度很低。

本文就是一时兴起，想写一篇给 Javaer 的 Rust 入门，很多 Java 开发者，都对这门语言感兴趣，但是可能因为它的学习路线非常陡峭而放弃。事实也确实如此，这门语言里的不少概念，比如所有权、借用检查、生命周期，对 Java 程序员来说是完全陌生的，但反过来想，Rust 里也有大量的东西，我们其实在 Java 里早就见过了，只是名字不同而已。还有这几年 Java 的升级，其实也借鉴了很多 Rust 上的东西。

而且现在有了 AI Coding 的加持，其实我们不必像过去一样，非常精通一门语言才能开始使用它，只要能看得懂语法，知道它的玩法，它就可以成为你的一个技能点。

我工作这么多年，主力语言一直是 Java 和前端的一些技术栈。两三年前开始使用 Rust，我觉得它是非常有趣的语言，是那种能带给我们新的思考的语言。

我希望本文是一篇有趣的文章，也是一篇有用的文章。我会通过对比大家熟悉的 Java，来帮助大家理解 Rust 的各种内容。

作为本文的读者，默认你写过几年 Java，对 JVM、Maven、泛型、lambda、并发这些都了解。本文不会讲“在 Rust 里面什么是变量？”这种东西，但是会和 Java 做不少的对比，再加上要学习的概念确实不少，所以本文不会太短。感兴趣的可以按章节慢慢看，不一定要一口气读完，我争取让每一节都足够简单好理解，还有就是很多细节可能看第一遍的时候是不太懂的，这其实也没太大的关系，看完全文再倒回去看可能就豁然开朗了。

## Rust 和 Java 是不同的设计方向

学 Rust 的时候最大的感受不是语法难，而是它老是在逼你换一种写代码的方式。

写 Java 代码，大家脑子里都是这些：

- 对象统统在堆上，变量里放的是引用。
- 对象什么时候释放，交给 GC，我们不关心。
- 参数传来传去，本质都是引用拷贝，多个变量可以指向同一个对象。
- 多线程共享对象，靠 `synchronized`、`Lock`、`volatile`、并发容器兜底。
- 空值用 `null`，异常用 `try/catch`。

这些东西太自然了，自然到我们平时根本不会多想。Rust 麻烦就麻烦在这里，它几乎把这些全部改掉了。Rust 的设计其实在做一件很朴素的事：把 Java 里很多运行时才会暴露的问题，提前到编译期解决掉：

- Java 里可能 NPE，Rust 用 `Option<T>` 逼你处理。
- Java 里可能忘记 catch，Rust 用 `Result<T, E>` 逼你处理。
- Java 里可能两个线程同时改一个对象，Rust 用所有权和 `Send`/`Sync` 逼你说清楚。
- Java 里对象什么时候被释放靠 GC，Rust 在编译期就把每个值的销毁点算清楚。

也就是说，这两门语言架构上的核心差别就一句话：**Java 中间有 JVM 和 GC 帮你兜底，而 Rust 让编译器在编译期把规则检查完**。这也是为什么 Rust 的编译器看起来很烦，初学者会一直在跟编译器做斗争，想要写出可编译的代码可能就已经要了老命了。

大家把这几条记心里就行，后面我们会逐步介绍其细节。

## 一、工具链：rustup 就是 Rust 的 SDKMAN

我们先来看看 Rust 的工具链，这部分其实最容易上手，因为基本就是 Java 生态那一套换了个名字。

Java 这边我们装环境，先有 JDK，里面带 javac、java、jar、javadoc、jshell 这些工具；如果要管理多个 JDK 版本，会用 SDKMAN 或者 jenv。

Rust 这边对应的关系是这样的：

- `rustup`：版本管理工具，对应 SDKMAN/jenv，负责下载、切换 Rust 工具链
- `rustc`：编译器，对应 javac
- `cargo`：构建+包管理，对应 Maven 或 Gradle（这个非常重要，后面单独讲）
- `rustfmt`：格式化，对应 google-java-format
- `clippy`：lint 工具，对应 SonarLint / SpotBugs
- `rust-src`、`rust-docs`、`rust-std`：源码、文档、标准库

这一整套东西打包在一起叫 toolchain，统一通过 rustup 来管理。我们可以看到，Rust 把“代码风格一致性”当成语言体验的一部分，直接在 toolchain 中包含了 format、lint 等工具，让大家可以更好地管理代码风格与约束。

Rust 还分 stable、beta、nightly 三个版本，大致可以这么理解：

- stable：对应 Java 的 LTS，正常项目用这个
- beta：下一个 stable 候选版本
- nightly：每天构建的最新版，对应 Java 的 EA（early access），有些实验特性只在 nightly 上能用

常用命令我们看一眼：

```shell
# 安装 rustup（顺带把 stable 工具链装上）
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

# 看版本，这个跟 java -version 一个意思
rustc --version

# 升级，rustup 会顺便把 cargo 也升了
rustup update

# 看当前用的是哪个 toolchain
rustup show

# 看当前 toolchain 装了哪些组件
rustup component list --installed

# 装 clippy（默认其实就装好了）
rustup component add clippy

# 切换版本，类似 sdk use java xxx
rustup install 1.90.0
rustup default 1.90.0
rustup default beta    # 切换到 beta
rustup default nightly # 切换到 nightly

# 只在当前目录用某个版本，类似项目级别的版本配置
rustup override set nightly
rustup override set 1.95.0
```

工具链装好以后，rustfmt 和 clippy 就可以直接用了：

```shell
cargo fmt     # 代码格式化
cargo clippy  # 代码质量检查
```

我们是可以给 rustfmt 和 clippy 定制规则的，这是后话了，不做介绍。

到这里，工具链层面我们就跟 Java 对上号了。下面我们看看怎么写第一个程序。

## 二、Hello World

老规矩，写完 Hello World，就算入门 Rust 了。

写个 main.rs：

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

然后编译：

```shell
rustc main.rs
```

这里要注意三个点：

1. 入口函数叫 `main`，写法是 `fn main()`，跟 Java 几乎一样。
2. `println!` 后面有个感叹号，说明它不是普通函数，是个宏（macro），后面会讲。
3. 编译出来的是直接可执行的二进制文件，不像 Java 的 `.class` 字节码，需要 JVM 才能跑。

```shell
./main
```

这就是 Rust 的第一个核心区别：它没有运行时虚拟机，没有 GC，跟 C/C++ 一样编译成原生代码直接跑。这也是为什么 Rust 适合做系统编程、底层服务、CLI 工具，而 Java 由于 JVM 的存在，在启动速度和资源占用上一直是劣势。

不过，真实工程里我们基本不会直接用 rustc 来编译，就跟我们在 Java 里几乎不会直接用 javac 一样，都是用构建工具来组织。Rust 的构建工具叫 cargo。

## 三、Cargo：Rust 世界的 Maven

在装 rust 的时候顺手就装好了 cargo，跑 `rustup update` 升级 Rust 的时候 cargo 也跟着升。

我们直接看用法：

```shell
cargo new hello_cargo
```

这个相当于 `mvn archetype:generate`，生成一个项目骨架。它其实只是帮我们创建 `src/main.rs` 和 `Cargo.toml`。

Cargo.toml 跟 pom.xml 的角色完全一样，都是项目配置文件，只不过格式是 TOML。看一眼：

```toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024" # Rust的语言版本，越高的版本支持越多的语法，目前最新是 2024

[dependencies]
# 在 java 里我们叫 jar 包，在 rust 里叫 crate
rand = "0.8.5"
```

> 注意，edition 版本是语言层面的，不是前面说的 rustup 管理的 rustc、cargo、rustdoc, clippy 等 toolchain 的版本。当然了，你如果使用 edition=2024，肯定有最低的编译器版本要求的，不然旧版的编译器可能都不认识 edition 2024 新增的语法，根本没法编译。

接着是构建命令：

```shell
cargo build           # 类似 mvn compile，默认编译出一个 debug 版本
cargo build --release # 类似 mvn package -P release，会做优化、去掉调试信息等，生成一个最终可用版本
cargo run             # 类似 mvn exec:java，build + run 一条龙
cargo check           # 只检查能不能过编译，不生成产物，速度快得多
cargo clean           # 类似 mvn clean，把 target 目录删掉
```

构建产物在 `target/debug/` 或 `target/release/` 下面。

这里要特别提一下 `cargo check`。Rust 的编译比 Java 慢得多（因为它要做单态化、借用检查这一堆事），所以日常写代码时常用 `cargo check` 看类型和借用规则能不能过，速度会快很多。等到要真正跑了再 `cargo build`。

依赖管理这块我们重点看一下：

```shell
cargo add axum@0.7.2  # 类似 mvn dependency:add（其实 javaer 都是手动复制粘贴依赖的），加依赖到 Cargo.toml
cargo update          # 按 SemVer 兼容范围升级：1.2.3（即 ^1.2.3）会升到 <2.0.0；0.8.5（即 ^0.8.5）会升到 <0.9.0
cargo tree            # 类似 mvn dependency:tree
cargo doc --open      # 生成依赖的文档并打开，介绍各个API，其实没啥用，至少Java的docs，我们现在几乎是不看的
```

这里有个 `Cargo.lock` 文件，作用跟 npm 的 package-lock.json 一样，确保依赖版本固定。Maven 没有这个东西，因为 Maven 的版本号本身就是固定的（除了 SNAPSHOT），但 Cargo 的版本号是语义化的范围（"0.8.5" 实际表示 ">=0.8.5, <0.9.0"），所以需要 lock 文件来固化。

> 举个例子，假设我们第一次 build 的时候，没有 Cargo.lock 文件，编译器会去找一个满足条件的最大版本，假设找到了 0.8.9，那么这个版本号会写入到 Cargo.lock 文件，以后都会固定使用 0.8.9，即使以后有 0.8.100 我们也不会更新，因为版本被 lock 了，除非我们手动跑 cargo update，它才会更新 lock 文件。但还是那句话，如果你跑 cargo update 的时候，有 0.8.100 和 0.9.0，编译器会使用 0.8.100。另外，如果我们要精确使用 0.8.5，应该这么写 `rand = "=0.8.5"`。
>
> 简单理解 SemVer 的规则：对于 0.y.z 的版本，更新最后一个数字到最新的，对于 x.y.z，如 1.2.5，它的语义范围是 [1.2.5, 2.0.0)

应用项目一般要把 `Cargo.lock` 提交到 git，库项目通常不太关心这个，按项目习惯来。

如果要用私有 registry，类似 Maven 的私服：

```toml
[dependencies]
my_crate = { version = "1.0", registry = "private-registry" }

[registries]
private-registry = { index = "https://your-private-cargo-registry.com" }
```

这里有一个跟 Java 差异很大的地方需要特别说一下：**Rust 不允许你"白嫖"上游的传递依赖**。

什么意思呢？即使 a 依赖了 c，你的项目想用 c 的 API，也必须自己也在 Cargo.toml 里再写一遍 c。这样做的好处是，上游用了哪些依赖是它自己的实现细节，以后它升级或者换掉 c，都不会破坏你的代码。

再来看版本冲突的场景：假设你的项目依赖了 a 和 b，a 和 b 都依赖了 c，但是版本不一样。

在 Java 里，Maven 会做 dependency mediation，最终选一个版本，然后大家都用这个版本，可能就引发各种 NoSuchMethodError。

在 Rust 里，Cargo 会下载两个版本的 c，a 和 b 在链接的时候各自用各自的版本，不会有 Java 那种"依赖地狱"的问题。

跟 Java 还有一点很不一样的是包的设计风格。Rust 生态更喜欢把一个包做大、提供大量 features，使用方按需开启，比如 tokio：

```toml
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```

这个习惯的成因是：Rust 是静态编译的，编译器会做 dead code elimination，没用到的 feature 在最终产物里根本不存在。所以一个大包加 features 反而更高效，依赖管理也更简单。Java 那边就没这个传统，大家都做小包，比如 Spring 就有非常多的 jar 包。

Clippy 我们前面提过一嘴，它是 toolchain 的一部分，它就是 Rust 的 SonarLint，做静态分析、检查各种 idiom、发现潜在 bug，跑下面的命令：

```shell
cargo clippy
```

我们也可以通过 `clippy.toml` 配置自己想要的规则。一般来说 IDE 插件会自动集成，不需要你手动跑。

最后看一个文件：`rust-toolchain.toml`。这个文件放在项目根目录，作用是锁定项目用的 Rust 版本：

```toml
[toolchain]
channel = "1.90.0"
targets = ["x86_64-unknown-linux-gnu"]
components = ["rustfmt", "clippy"]
```

如果别人 clone 你的项目，本地没有 1.90.0，cargo 会自动下载安装。这个有点像 Java 项目里的 `.sdkmanrc` 或者 Maven 的 enforcer 插件，但更原生、更可靠。

好了，工具链就到这里。下面我们正式进入语言本身。

## 四、基础语法：跟 Java 大同小异

Rust 的语法表面上跟 C/Java 系还是比较像的，只不过 Rust 的类型放在变量名后面，主要原因应该还是能推导就推导，这样不用显式写类型。

变量定义用 `let`，默认**不可变**（跟 Java 反过来，Java 默认可变，要加 final 才不可变）：

```rust
let x = 5;
x = 6; // 编译错误，因为 x 是不可变的

let mut y = 5; // 加 mut 表示可变
y = 6; // OK
```

在 Java 中，我现在也喜欢用 var 关键字，让编译器自动做类型推导，个人觉得 Java 还是肯吸收别的语言的好东西的。

`mut` 这个关键字以后会反复出现，它就是"可变"的意思。为什么 Rust 要这么设计？简单说，Rust 希望你默认写出"少共享、少修改"的代码。变量能不改就不改，引用能只读就只读。后面讲并发时你会发现，这个习惯和 Rust 的安全模型是连在一起的。

Rust 还有一个 Java 没有的概念叫 **shadowing**，就是同名变量复用：

```rust
let x = 5;
let x = x + 1;     // 这是一个新变量，跟原来的 x 没关系，只是名字一样
let x = "hello";   // 类型甚至可以变
```

shadowing 在处理“同一个数据，类型变了”的场景特别有用。Java 里我们经常这样写：

```java
String text = "123";
int value = Integer.parseInt(text);
int result = value + 1;
```

这个代码很讨厌的就是，我们一直在取新的名字。Rust 允许你在同一条处理链上一直用 `x` 这个名字，对应类型可以变。这个东西刚开始看有点怪，用多了会觉得还挺顺。

数据类型这块，Rust 的整数类型比 Java 细：

```
i8, u8           // 有符号/无符号 8 位
i16, u16
i32, u32         // 默认整数类型
i64, u64
i128, u128
isize, usize     // 跟平台位宽一致，用作下标的就是 usize
f32, f64         // 浮点，默认 f64
bool, char       // 注意 char 是 4 字节的 Unicode 标量值，不是 ASCII 字符！
```

Java 没有无符号整数类型，Rust 这里就完整多了。另外 char 这块，Java 的 char 是 2 字节、只能表示 BMP，Rust 的 char 直接是 4 字节，能塞下中文、emoji。

复合类型：

```rust
// 元组：异构
let tup: (i32, u32) = (100, 1);
let first = tup.0;   // 通过 .0 .1 访问

// 数组：定长、同构
let a: [i32; 5] = [1, 2, 3, 4, 5];
let b = a[1];        // 越界访问会 panic（相当于 Java 抛 ArrayIndexOutOfBoundsException，但 Rust 用的是 panic 机制，不是受检异常）
```

在 java 中，可以用 record 实现类似 tuple 的效果。

字面量这里有几个 Rust 特有的写法：

```
123_456    // 下划线分隔，可读性
0xff       // 十六进制
0o123      // 八进制
0b11100    // 二进制
b'A'       // 单字节字符（u8）
b"abc"     // 字节字符串字面量，类型是 &[u8; 3]，可以强转为 &[u8]
```

函数定义：

```rust
fn plus_one(x: i32) -> i32 {
    x + 1
}
```

这里有个非常 Rust 特色的细节：**函数最后一行不带分号，就是返回值**。

```rust
fn main() {
    let y = {
        let x = 3;
        x + 1   // 注意没分号，整个块的值是 4
    };
    println!("y = {y}");
}
```

还有 {} 块本身就是表达式，上面这个例子里面 x + 1 就是这个块的返回值，赋给了 y。Java 里 `{}` 是语句块，没这个语义。

带分号的情况下，那一行就变成语句，值是 `()`，可以简单理解为 Java 的 `void`。所以下面这段就编译不过：

```rust
fn add(a: i32, b: i32) -> i32 {
    a + b;   // 错误，函数声明返回 i32，但这里返回了 ()
}
```

if 在 Rust 里也是表达式（再次和 Java 不同）：

```rust
let number = if condition { 5 } else { 6 };
```

这种东西在 Java 里我们用三元运算符 `condition ? 5 : 6` 来表达。

循环用 `loop`、`while`、`for`，最好用的是 `for`：

```rust
let a = [10, 20, 30, 40, 50];
for element in a {
    println!("the value is: {element}");
}
```

常量用 `const`，跟 Java 的 `static final` 类似：必须显式指定类型，必须在编译期能确定值，不能用 mut：

```rust
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
```

全局变量用 `static`，跟 Java 的差不多，必须声明时初始化：

```rust
static G1: i32 = 10;

// 也可以 mut，但这样的话，读写都得用 unsafe（后面讲 unsafe）
static mut G2: i32 = 0;
unsafe {
    G2 = 5;
}
```

需要补充一句：`static mut` 在 Rust 2024 edition 已经被强烈不建议使用了，对它取引用会直接报错。如果真的需要共享可变全局状态，更推荐 `Mutex`、`RwLock`、`OnceLock` 这些类型，知道有这么个东西就行，先不深入。

跟 const 的区别：const 可能被编译器内联进每个使用点，static 就是个真实的内存地址。

注释用 `//` 或 `/* */`，跟 Java 一样。

到这里，跟 Java 大同小异的部分基本介绍完了。下面进入 Rust 的核心，也是 Java 程序员最不熟悉的部分。

## 五、Ownership：Rust 最有特色、也是最劝退的概念

Java 程序员一上来最容易被劝退的就是这块。我们慢慢来。

### Java 是怎么管理内存的？

我们先回忆一下 Java。Java 的对象都在堆上，栈上只放引用：

```java
User u1 = new User("javadoop");
User u2 = u1;
```

这两行做的事，我们都很熟悉：堆上有一个 `User` 对象，栈上 `u1` 和 `u2` 这两个引用指向它。

```text
栈上：

u1 ─┐
    ├──> 堆上的 User("javadoop")
u2 ─┘
```

对象什么时候被回收？GC 决定。GC 通过可达性分析判断哪些对象没人引用了，然后回收它们的内存。

GC 的好处是开发者不用关心内存释放，写起来爽。坏处是：

- 有 STW，对延迟敏感的场景不友好
- 内存占用偏大
- 不够确定，你不知道一个对象什么时候真正被释放

所以 Java 程序员习惯了一个事实：引用可以随便复制，对象释放不用自己管。

但是 Rust 没有 GC，它也不像 C 那样让你 `malloc`/`free`，那样太容易出错。Rust 的方案是编译期就确定每个值什么时候被释放，靠的是一套叫所有权（ownership）的规则。

Rust 必须回答一个问题：一块堆内存，到底谁负责释放？

如果有多个变量都觉得自己"拥有"这块内存，那就麻烦了。释放一次，另一个变量就成了悬垂指针；释放两次，就是 double-free；都不释放，就是内存泄漏。Rust 的答案非常直接：一个值，在任意时刻只能有一个 owner。

三条规则，先背下来：

1. Rust 中的每一个值都有一个所有者（owner）
2. 值在任意时刻有且只有一个所有者
3. 当所有者（变量）离开作用域，这个值就会被丢弃

是不是看起来还挺简单？我们看具体例子。

### 移动（move）

对基础类型，没什么好说的，直接拷贝：

```rust
let x = 5;
let y = x;  // 栈上有两个 5，x 和 y 都能用
```

但是对于 String 这种堆上分配的类型：

```rust
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}"); // 编译错误！s1 已经被 move 给 s2 了
```

这里发生了什么？String 内部是一个 `(指针, 长度, 容量)` 的结构，`let s2 = s1` 这一行，从 Java 视角看就是一次浅拷贝（s1 和 s2 都指向同一块堆数据）。但 Rust 不允许两个变量同时拥有同一份堆数据（规则 2），所以它**让 s1 失效**，这个操作叫 move。

![move](https://assets.javadoop.com/imgs/20510079/rust-for-java-developers/move.png)

注意，这里堆上的 `"hello"` 没有复制一份，只是 owner 变了。

为什么要这么做？想象一下，如果 s1 和 s2 都指向同一块堆内存，s1 离开作用域时它要不要释放堆内存？如果释放了，s2 就指向了悬垂指针。如果不释放，那要靠谁来释放？这就是 C++ 里 double-free 问题的根源。Rust 通过"单一所有者"直接绕开了这个问题。

如果你真的想要两份独立的数据，用 `clone`：

```rust
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{s1} and {s2}"); // OK，s1 还能用
```

这就是 Java 里的深拷贝。

### Copy trait

那基础类型为什么不用 clone 也能继续用呢？因为它们实现了 Copy trait（先别管 trait 是什么，类似 Java 的接口），只要类型实现了 Copy，赋值操作就是在**栈**上的按位复制，不发生 move。

i32、bool、char、f64 这些基础类型都实现了 Copy。元组只要里面的元素都是 Copy，元组本身也是 Copy。

简单记几条：

- `i32`、`bool`、`char` 这类基础类型通常是 `Copy`。
- `String`、`Vec` 这类拥有堆内存的类型通常不是 `Copy`。
- 不是 `Copy` 的类型，赋值、传参、返回值都可能发生 move。

### 引用和借用 borrow

每次都 move 来 move 去，写起来简直是噩梦。来看这段代码：

```rust
fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)  // 因为 s1 已经被 move 进函数了，所以得返回回去
}
```

这种写法多多少少有点大病。我只是想知道字符串的长度，结果还得把字符串本身传出来再传回去。所以 Rust 提供了**引用**：

```rust
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // 传引用
    println!("The length of '{}' is {}.", s1, len); // s1 还能用
}

fn calculate_length(s: &String) -> usize {
    s.len()
}
```

`&s1` 表示"我借给你 s1，但所有权还在我这里"。这个动作叫 **borrow**（借用），符号是 `&`。

简单类比：在 Java 里，方法参数传对象，本质就是传一个引用，方法内部可以访问对象，但对象的"主人"还是外面的代码。Rust 的引用大体上就是这个意思，只不过多了一层规则约束。

默认引用是不可变的。要修改，得用可变引用：

```rust
fn main() {
    let mut s = String::from("hello");
    change(&mut s); // 注意 mut 关键字到处都要写
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
```

可变引用这块，`mut` 写了两次，初学时很烦：

- `let mut s` 表示变量 `s` 可以被修改。
- `&mut s` 表示把它以可变引用的方式借出去。

Rust 的借用规则可以用一句话概括：

> 同一时间，要么有任意多个不可变引用，要么只有一个可变引用，但不能同时有两者。

Rust 借用规则就是把这种事情提前拦住。只要有人在读，就不能同时拿一个可变引用去改；只要有人在改，就不能再让别人读或改。

多个只读引用可以：

```rust
let mut s = String::from("hello");
let r1 = &s;     // 不可变引用
let r2 = &s;     // 不可变引用，可以
println!("{r1}, {r2}");
```

一个可变引用也可以：

```rust
let mut s = String::from("hello");
let r = &mut s;
r.push_str(", world");
```

但是读写混在一起不行：

```rust
let mut s = String::from("hello");
let r1 = &s;         // 不可变引用
let r2 = &mut s;     // 编译错误！有不可变引用时不能要可变引用
println!("{r1}");
```

两个可变引用也不行：

```rust
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;     // 编译错误！同时两个可变引用
```

这个模型跟读写锁长得很像，但它不是运行时真的加了一把锁，它是编译器在检查引用关系。

引用的作用域是从声明开始到最后一次使用为止，叫做 NLL（Non-Lexical Lifetime）：

```rust
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // r1, r2 最后一次用在这里
let r3 = &mut s; // OK，因为 r1, r2 已经不再使用
```

读到这里，所有权和借用的核心规则就讲完了。这部分初学的时候必然会跟编译器打架，习惯就好。Rust 编译器的报错信息写得相当友好，按提示改基本能过。

最后强调一点：借用检查全部是**编译期**完成的，运行时不会有任何额外开销，这是 Rust 一个非常重要的卖点。Java 那边为了实现线程安全，运行时要加各种锁、做各种 happens-before 同步，性能上是要付出代价的。Rust 把这些事情提前到编译期解决，运行时的代码就是干干净净的原生代码。

## 六、Struct：Rust 的"类"

Struct 就是 Rust 用来组织数据的方式，相当于 Java 的 class 但是只有字段没有方法。方法是在 impl 块里定义的。

定义和使用：

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

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

// 结构体更新语法，类似 Java 里 Lombok 的 @With
let user2 = User {
    email: String::from("another@example.com"),
    ..user1   // 其余字段从 user1 取
};
```

注意：`..user1` 这个操作可能会发生 move，比如 username 是 String，会被 move 到 user2，user1.username 之后就不能用了。

字段速记法（field init shorthand），跟 ES6 一样：

```rust
fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,   // 等价于 username: username
        email,
        sign_in_count: 1,
    }
}
```

Rust 还有几种特殊的 struct：

```rust
// 元组结构体，没有字段名
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

// 单元结构体，没有任何字段，类比 Java 的标记类
struct AlwaysEqual;
```

如果 struct 字段是引用，必须标注生命周期（后面讲），下面这个会报错：

```rust
struct User {
    username: &str,   // 编译错误：missing lifetime specifier
}
```

因为 username 是引用类型，而我们没有给它标注生命周期。先知道有这么个东西就行，在介绍生命周期之前，我们先不要在 struct 上涉及引用，直接让 struct 拥有值。

### 方法和 self、&self、&mut self

Java 里方法跟字段都写在 class 里面，混在一起。Rust 不这么搞，数据放 struct，而方法放 `impl` 块里：

```rust
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

let rect = Rectangle { width: 30, height: 50 };
rect.area();
```

这里我们重点看一下 `&self` ，它是 Rust 方法签名里最关键的部分。

类比 Java：Java 方法里的 `this` 是个隐式参数，永远是一个引用，而且方法默认就能改字段。Rust 里的 self 是显式参数，并且分了三种形式：

- `self`：拿走所有权（method 调用完就消费掉对象了）
- `&self`：不可变借用（最常用）
- `&mut self`：可变借用

```rust
struct Foo(i32);

impl Foo {
    fn new() -> Self {           // 没有 self，叫"关联函数"，类比 Java 静态方法
        Self(0)
    }

    fn consume(self) -> Self {   // 拿走所有权，调用后原对象不能再用
        Self(self.0 + 1)
    }

    fn get(&self) -> &i32 {   // 不可变借用，最常见
        &self.0
    }

    fn get_mut(&mut self) -> &mut i32 {  // 可变借用，需要修改字段时用
        &mut self.0
    }
}
```

调用方式：

```rust
let foo = Foo::new();   // 关联函数用 :: 调用
foo.get();              // 实例方法用 . 调用，自动加引用。它其实就是 (&foo).get()
Foo::get(&foo);         // 等价写法，把实例方法当普通方法使用，只不过就是传一个实例进去
```

`&self` 其实就是 `self: &Self` 的缩写，方法调用时编译器自动加 `&`。这跟 Java 的 `this` 自动传入是一个意思。

Java 里 `this` 永远是引用，方法默认就能改字段。Rust 把这件事拆开了：

- 只是读，就写 `&self`
- 要改，就写 `&mut self`
- 要消费整个对象，就写 `self`

我们看几个标准库的例子：

- `String::len(&self)`：只读取长度，用 `&self`
- `String::push_str(&mut self, ...)`：在原字符串上追加，用 `&mut self`
- `String::into_bytes(self)`：把 String 拆成 `Vec<u8>`，原 String 没用了，用 `self`

这个设计刚开始有点啰嗦，但它让 API 的意图非常清楚。你看到方法签名，就知道它会不会修改对象，会不会拿走对象。这是 Rust API 设计上一个非常贴心的地方。

跟 Java 不一样的还有一点，一个 struct 可以有**多个 impl 块**。比如你可以把方法按功能分到不同的 impl 块里，编译器最终会合并。

方法调用时 Rust 会自动引用和解引用，下面两个等价：

```rust
p1.distance(&p2);
(&p1).distance(&p2);
```

### 关联函数（associated function）

不带 self 的就是关联函数，类比 Java 的 static 方法：

```rust
impl Rectangle {
    fn square(size: u32) -> Self {
        Self { width: size, height: size }
    }
}

let r = Rectangle::square(10);
```

`String::from`、`Vec::new` 这些都是关联函数。最常见的用途就是构造函数，但也可以是和这个类型相关的工具函数（比如 `i32::from_str_radix`，把字符串按指定进制解析成整数）。

### 打印结构体

Java 里我们重写 toString 来打印对象。Rust 这边我们用 Debug 或 Display trait（trait 后面讲，先认住语法）：

```rust
#[derive(Debug)]   // 自动派生 Debug
struct Rectangle {
    width: u32,
    height: u32,
}

let rect = Rectangle { width: 30, height: 50 };
println!("{rect:?}");   // Rectangle { width: 30, height: 50 }
println!("{rect:#?}");  // 多行展开，更清晰
```

`#[derive(...)]` 是属性宏，类似 Lombok 的 `@Data`，自动给你生成代码。上面这种 :? 或者 :#? 使用的就是 Debug trait 的输出。

当然如果我们给 Rectangle 实现 Display trait，我们也可以这样：

```rust
use std::fmt;

impl fmt::Display for Rectangle {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} x {}", self.width, self.height)
    }
}

println!("{}", rect); // 使用 Display trait 打印结构体
println!("{rect}");   // 或者这样写，它们是等价的
```

## 七、Enum：比 Java 强大得多的枚举

Java 的 enum 你可以理解成"值就这几个的常量类"。Rust 的 enum 远不止于此，每个枚举变体可以带数据，而且数据形式可以不同。

```rust
enum Message {
    Quit,                          // 不带数据
    Move { x: i32, y: i32 },       // 带具名字段（像 struct）
    Write(String),                  // 带一个 String
    ChangeColor(i32, i32, i32),    // 带元组
}
```

是不是很强？这种 enum 在 Java 里只能用 sealed interface + record 来模拟（Java 17+），相当啰嗦：

```java
sealed interface Message permits Quit, Move, Write, ChangeColor {}
record Quit() implements Message {}
record Move(int x, int y) implements Message {}
record Write(String text) implements Message {}
record ChangeColor(int r, int g, int b) implements Message {}
```

enum 也可以加方法：

```rust
impl Message {
    fn call(&self) {
        // ...
    }
}

let m = Message::Write(String::from("hello"));
m.call();
```

### Option：替代 null 的存在

Rust 没有 null。要表达"可能有值，可能没有"，用 `Option<T>`：

```rust
enum Option<T> {
    None,
    Some(T),
}
```

这跟 Java 的 `Optional<T>` 思路一样，但 Java 的问题是：就算用了 Optional，别人还是可以给你传 null，编译器是没有任何保证的。Rust 不一样，普通类型就是普通类型，`Option<T>` 就是可能为空的类型，二者在类型系统里分得很清楚。

这点对写代码的影响非常大。Java 里你看到：

```java
User findUser(long id);
```

你不知道它找不到时会怎么样：

- 返回 null？
- 抛异常？
- 返回一个空对象？

Rust 里如果可能找不到，签名通常会写成：

```rust
fn find_user(id: u64) -> Option<User>;
```

这就很清楚了：调用方必须处理 `None`。

`Option` 是 prelude 里默认导入的，直接用：

```rust
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None; // 赋空值，类似 Java 中的 Integer absent_number = null;
```

要拿出 Option 里的值，得用 `match`：

```rust
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1), // 解构里面的数据
    }
}
```

### match：增强版 switch

`match` 大致对应 Java 的 `switch`，但更强：

- 必须穷尽所有可能（编译期检查）
- 可以解构（destructure）出数据
- 是表达式，可以赋值

```rust
let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    other => move_player(other),  // 通配符，绑定到 other
    // _ => ...                   // 不关心值时用 _
}
```

Java 17 里加的 pattern matching 跟 Rust 思路相似，其实都是在借鉴 ML/Haskell/Scala 这些函数式语言的成熟设计，目前 Java 的完整度还差不少。

### if let

只关心一个分支的时候，用 match 显得啰嗦：

```rust
let a = Some(10);
match a {
    Some(v) => println!("v: {v}"),    // 有值的时候进行打印
    _ => (),                          // 没值的时候直接忽略
}
```

这种情况用 `if let` 简化：

```rust
let a = Some(10);
if let Some(v) = a {
    println!("v: {v}");
}
```

`if let` 是 match 的语法糖，代价是放弃了穷尽性检查。带 else 分支也行：

```rust
if let Some(v) = a {
    // ...
} else {
    // ...
}
```

`while let` 同理，常用于循环消费 `Option`：

```rust
while let Some(top) = stack.pop() {
    println!("{top}");
}
```

### Result：替代异常的存在

前面我们说过，Rust 没有 Java 的 `try/catch`。可能失败的操作返回的是 `Result<T, E>`：

```rust
enum Result<T, E> {
    Ok(T),
    Err(E),
}
```

`Ok(T)` 表示成功并带上结果，`Err(E)` 表示失败并带上错误信息。Result 跟 Option 思路完全一样，只不过 Option 不关心"为什么没值"，Result 关心"失败的原因"。

举个例子，文件读取在 Java 里要么 `throws IOException`，要么调用方 `try/catch`：

```java
String content = Files.readString(Path.of("hello.txt"));
```

到 Rust 里，签名长这样：

```rust
fn read_to_string(path: &Path) -> Result<String, io::Error>;
```

调用方必须处理 `Result`：

```rust
match std::fs::read_to_string("hello.txt") {
    Ok(content) => println!("{content}"),
    Err(e) => eprintln!("读取失败：{e}"),
}
```

初学者肯定会觉得写 match 非常啰嗦，而且代码里面肯定有大量的 Option 和 Result，都这么写，会非常麻烦。所以 Rust 提供了 `?` 操作符：

```rust
fn read_username() -> Result<String, io::Error> {
    let content = std::fs::read_to_string("user.txt")?; // 出错就提前 return Err
    Ok(content.trim().to_string())
}
```

比如在 Result 上，`?` 后缀的语义是：拿到 `Ok` 就把值解包出来继续往下走，拿到 `Err` 就把错误直接 return 给调用方。自己不处理，交给上层处理，这相当于 Java 里的 `throws` 自动传播，只不过 Rust 把它做成了表达式级别的语法糖，而且必须显式标记每一处可能失败的调用，不会出现"忘了 catch"的情况。

当 `?` 作用在 `Option` 上的时候，它的含义是"如果这个 Option 是 None 就直接 return None"，思路一致。

## 八、Panic：不可恢复错误的兜底机制

前面我们讲了 Result，处理的是"调用方有机会救一下"的错误，比如文件没找到、网络超时、解析失败。但还有一类错误是真的没法救的，比如数组越界、整数除零。继续跑下去也没意义，Rust 用的是 panic 机制。

简单理解，panic 跟 Java 里"抛了个 RuntimeException 没人接，程序挂掉"差不多，但细节上有些区别。

### 什么时候会发生 panic

panic 主要分两类：显式触发，和运行时自动触发。

显式触发的就是直接调用 `panic!` 宏，或者调用了 Result/Option 上那些"出了错就 panic"的快捷方法：

```rust
panic!("出大事了");                       // 显式 panic

let v: Vec<i32> = vec![1, 2, 3];
let x = v[100];                           // 越界，自动 panic

let s: Option<i32> = None;
let n = s.unwrap();                       // None 上 unwrap 会 panic
let n = s.expect("这里不应该是 None");     // 同 unwrap，但带自定义提示信息

todo!();         // 占位用，运行到这里就 panic，提醒你这部分还没写
unreachable!();  // 表达"这里逻辑上不可能执行到"，万一执行到就 panic
```

`unwrap` 和 `expect` 在写小工具或者快速验证想法的时候很方便，但正式代码里要小心，它们就是把 None/Err 直接转成 panic 的快捷方式。

运行时自动触发 panic 的常见场景，比如整数除零，或者数组、Vec、Slice 越界，这些都是运行的时候才会触发。

### panic 发生后会怎么样

默认情况下，panic 会做这么几件事：

1. 打印错误信息和文件位置到 stderr
2. 沿着调用栈往上"展开"（unwinding），把每一层栈帧上的对象都按规则 Drop 掉，文件句柄会关、Mutex 锁会解、堆内存会释放
3. 当前线程结束

注意第三点，是当前线程结束，不是整个进程。子线程 panic，主线程感知不到，除非主线程 join 它，会拿到一个 `Err`。但主线程 panic，整个进程就退出了。这点跟 Java 里 Thread 出现未捕获异常的行为类似。

panic 默认走 unwinding 是为了让析构函数能正常跑，但 unwinding 本身是有代价的：二进制会变大，性能也有点损耗。如果你不在乎这些，可以在 Cargo.toml 里改成 abort：

```toml
[profile.release]
panic = "abort"
```

这样 panic 直接调用系统 abort，跳过 unwinding，二进制更小、跑得更快，但析构函数都不会执行。嵌入式或者对包体积敏感的场景常用这个。

想看完整调用栈，跑程序的时候带上环境变量：

```shell
RUST_BACKTRACE=1 cargo run    # 简略 backtrace
RUST_BACKTRACE=full cargo run # 完整 backtrace
```

### Result 还是 panic：什么时候用哪个

Rust 官方有个挺清晰的指导原则：

- 调用方有可能合理处理的错误（文件没找到、网络超时、解析失败），返回 Result
- 程序不变量被打破、属于"代码 bug"的情况，直接 panic

举个例子，从用户输入解析数字，输入是不是数字调用方控制不了，应该返回 Result。而函数内部访问一个数组、且明确知道下标在范围内，万一越界那是代码 bug，让它 panic 就行。

实际写代码的几个习惯：

- 库代码尽量返回 Result，把"要不要 panic"的决定权交给调用方
- 应用代码可以适当 `unwrap`，但要用在能确认不会失败的地方
- 表达"这里逻辑上不可能执行到"用 `unreachable!()`
- 还在写但没写完的代码用 `todo!()` 占位，编译能过，运行时一执行就 panic

### 几个容易踩坑的点

第一个是**整数溢出**，前面提过一嘴，这里再展开说一下。debug 模式下溢出会 panic，release 模式下默认是回绕（wrap，比如 `u8::MAX + 1` 变成 0）。这意味着你在 debug 下跑得好好的代码，编译成 release 之后行为就完全不一样了，而且不会有任何提示。如果你需要明确的语义，用标准库提供的几个方法：

```rust
let a: u8 = 200;
let b = a.checked_add(100);     // 返回 Option，溢出就是 None
let c = a.saturating_add(100);  // 溢出就饱和到 u8::MAX
let d = a.wrapping_add(100);    // 显式回绕
let (e, overflowed) = a.overflowing_add(100); // 返回结果和是否溢出
```

第二个是 **panic 不要随便穿过 FFI 边界**。Rust 代码如果通过 C ABI 被外部调用，让 panic 直接展开到 C 那一侧是 undefined behavior。这种场景需要用 `std::panic::catch_unwind` 把 panic 接住、转成错误码返回：

```rust
use std::panic;

let result = panic::catch_unwind(|| {
    panic!("oops");
});

match result {
    Ok(_) => println!("正常返回"),
    Err(_) => println!("接住了一个 panic"),
}
```

注意，catch_unwind 不是 Java 的 try/catch，它只是给 FFI 边界用的安全网。日常的错误处理还是 Result + `?`，不要拿 catch_unwind 当 try/catch 来滥用。而且如果你设了 `panic = "abort"`，catch_unwind 也根本接不住。所以，我们最好不要用 Java 的思维，老想着用 try/catch，而是应该始终使用 Result 来显式传播。

到这里 panic 就介绍完了。一句话总结：Result 处理"可能失败的事情"，panic 处理"出 bug 了的事情"，二者分工明确，不要混用。

## 九、集合

集合这块对 Java 程序员来说不算特别陌生，HashMap、ArrayList 这些东西的概念是通用的，差别主要在 API 风格和所有权处理上。我们挑几个常用的过一下。

### Vec：对应 Java 的 ArrayList

`Vec<T>` 就是 Rust 的动态数组，对应 Java 的 `ArrayList<T>`，用法上没啥惊喜：

```rust
let v: Vec<i32> = Vec::new();         // 空 Vec
let v = vec![1, 2, 3];                // 用宏初始化，最常用
let mut v = Vec::with_capacity(10);   // 预分配容量，类似 new ArrayList<>(10)
```

增删改查这些基本操作：

```rust
let mut v = vec![1, 2, 3];
v.push(4);              // 尾部添加
v.pop();                // 弹出最后一个，返回 Option<T>
v.insert(0, 99);        // 在指定下标插入
v.remove(0);            // 删除指定下标
let first = &v[0];      // 索引访问，越界会 panic
let first = v.get(0);   // 安全访问，返回 Option<&T>
```

注意 `v[0]` 和 `v.get(0)` 的区别。`v[0]` 越界会直接 panic，`v.get(0)` 越界会返回 `None`。这种"同一个操作给两个 API，一个出事就崩，一个出事返回 Option"的设计在 Rust 标准库里非常常见，后面 HashMap、String 都能看到。

迭代有三种姿势，这是 Rust 比 Java 灵活的地方：

```rust
let v = vec![1, 2, 3];

// 1. 不可变借用，最常用
for x in &v {
    println!("{x}");
}

// 2. 可变借用，可以修改元素
let mut v = vec![1, 2, 3];
for x in &mut v {
    *x += 10;       // 通过解引用来修改
}

// 3. 拿走所有权，迭代完 v 就不能再用了
for x in v {
    println!("{x}");
}
```

这三种迭代方式分别对应方法 `iter()`、`iter_mut()`、`into_iter()`，是后面讲迭代器时会反复见到的三件套，先有个印象就行。

Vec 还有一堆函数式风格的链式方法，这块在 Java 里要靠 Stream API 才能写得这么顺：

```rust
let v = vec![1, 2, 3, 4, 5];
let sum: i32 = v.iter().filter(|&&x| x > 2).map(|x| x * 2).sum();
// 类似 Java: v.stream().filter(x -> x > 2).mapToInt(x -> x * 2).sum()
```

这里 `|&&x|` 那两个 `&` 看着很怪，先不用纠结，后面讲闭包和迭代器时会展开，跟着代码先有个感觉就行。

迭代器是 Rust 里非常重要的一个话题，展开讲会跑偏，这里就先到这。

### HashMap：用法跟 Java 几乎一样

`HashMap<K, V>` 用法上跟 Java 的 HashMap 没啥本质区别，先看基础操作：

```rust
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

// 遍历
for (key, value) in &scores {
    println!("{key}: {value}");
}

// 更新就是直接覆盖
scores.insert(String::from("Blue"), 25);
```

注意 HashMap 不在 prelude 里，要手动 `use std::collections::HashMap`，这点跟 Vec 不一样。

HashMap 的 insert 需要所有权，因为 insert 以后，对象归 HashMap 所有了。它的 get 操作要单独说一下。Java 里我们写 `map.get(key)` 拿到的就是 V 本身（要么是值，要么是 null），一步到位。Rust 这边写起来要绕几道弯，初学的时候很容易被绕晕，我们一步步来看。

第一层，最朴素的写法：

```rust
let v = scores.get("Blue");
```

这里 `v` 的类型是 `Option<&i32>`。看着挺唬人，拆开来其实就两件事：

1. **外层 `Option<...>`**：因为 key 不一定存在，找不到时返回 `None`，找到了返回 `Some(...)`。Rust 没有 null，所有"可能没有"的情况都用 `Option` 表达。
2. **内层 `&i32`** 而不是 `i32`：因为 value 是 HashMap 拥有的，`get` 只是借给你看一眼，不会把所有权交出去。这跟我们前面讲的借用规则是一致的，能借就借，少做不必要的复制。

要拿到真正可以参与运算的 `i32`，得把这两层都剥掉。最朴实的写法就是 match：

```rust
let v: i32 = match scores.get("Blue") {
    Some(&n) => n,    // 模式里写 &n，把外层 Option 和内层引用一起解掉，n 是 i32
    None => 0,         // 找不到给个默认值
};
```

每次都写 match 太啰嗦，标准库给了一组组合子方法，可以链式地把这两层处理掉。我们一层层往下加：

```rust
// 第二层：把内层 &i32 复制成 i32，但外层 Option 还在，得到 Option<i32>
let v: Option<i32> = scores.get("Blue").copied();

// 第三层：再给个默认值，把外层 Option 也剥掉，得到 i32
let v: i32 = scores.get("Blue").copied().unwrap_or(0);

// 或者：确信有值就直接 unwrap，找不到就 panic
let v: i32 = scores.get("Blue").copied().unwrap();
```

每加一步，类型就变一次，目标都是把 `Option<&i32>` 一步步剥成最终想要的 `i32`。

`copied` 这里要解释一下。它的作用是把 `Option<&T>` 变成 `Option<T>`，前提是 `T` 实现了 `Copy`（i32、bool 这种基础类型都实现了）。如果 value 是 `String` 这种非 Copy 类型，对应的方法叫 `cloned`，做一次深拷贝。注意一个是 copied 一个是 cloned。

```rust
let mut names: HashMap<i32, String> = HashMap::new();
names.insert(1, String::from("javadoop"));

let n: Option<String> = names.get(&1).cloned();
```

如果不想付出 clone 的代价，那就一直在引用层面操作，不剥内层：

```rust
let n: Option<&String> = names.get(&1);
if let Some(name) = n {
    println!("name: {name}");   // name 是 &String，println 会自动处理
}
```

刚开始写 Rust 看到这种 `.copied().unwrap_or(0)` 的链式调用会觉得啰嗦，写多了会发现这种"按需剥皮"的设计其实挺优雅，每一步剥的是什么、付出什么代价，都写得清清楚楚。Java 那边一个 `map.get(key)` 把所有事情都打包了，方便是方便，但是 null 检查、类型装箱拆箱这些事也都被藏起来了。

下面介绍一个 Rust 中非常好用的 entry API。在 Java 中，我们经常会见到这种代码：

```java
Integer count = map.get(key);
if (count == null) {
    map.put(key, 1);
} else {
    map.put(key, count + 1);
}
```

JDK 8 以后可以用 `map.merge` 或者 `computeIfAbsent` 来实现类似的需求，但这些方法各管一摊，没有形成一套统一的 API。Rust 这边官方就提供了一套统一的 entry API，把"查找 + 判断 + 插入/更新"这种组合操作打包成了原子操作：

```rust
// "如果不存在就插入"
scores.entry(String::from("Blue")).or_insert(50);

// "存在就 +1，不存在就插入 1"，统计词频经典写法
let text = "hello world hello";
let mut map = HashMap::new();
for word in text.split_whitespace() {
    *map.entry(word).or_insert(0) += 1;
}
// 这里 or_insert 返回的是 &mut V，所以 *xxx += 1 就是直接在 map 里改
```

第二个写法第一次看可能有点绕，但用熟了非常顺手，在 Rust 代码里随处可见。

再强调一下，HashMap 的所有权这块要稍微注意一下：

```rust
let key = String::from("hello");
let val = String::from("world");
let mut map = HashMap::new();
map.insert(key, val);
// 此时 key 和 val 都被 move 进 map 了，外面不能再用
```

如果不想 move，那就插入 clone 出来的副本，或者用引用作为 key/value（涉及生命周期，先不展开）。

### 其他集合简单过一下

标准库里 `std::collections` 下还有这些，跟 Java 基本能一一对上：

- `HashSet<T>`：哈希集合，对应 Java 的 HashSet
- `BTreeMap<K, V>`：基于 B 树的有序 Map，对应 Java 的 TreeMap
- `BTreeSet<T>`：有序 Set，对应 Java 的 TreeSet
- `VecDeque<T>`：基于环形缓冲区的双端队列，对应 Java 的 ArrayDeque
- `LinkedList<T>`：双向链表，跟 Java 一样标准库里有，但用得很少

用法跟你想的差不多，需要的时候查一下文档就行，这里就不展开了。

集合就讲到这。下一节我们专门聊聊字符串，按理说字符串也算一种集合，The Rust Book 也是把它放在 collections 章节里的，但 Rust 的 `String` 和 `&str` 区分本质上是所有权设计的体现，跟集合不是一个维度的事情，单独拉出来讲会更清楚一些。

## 十、字符串：String 和 &str

Rust 里跟字符串相关的核心类型有两个：`String` 和 `&str`，它们的关系大概是这样的：

- `String`：拥有所有权的可变字符串，数据存在堆上，概念上接近 Java 的 `StringBuilder`
- `&str`：字符串切片（slice），是对一段字符串数据的不可变借用

Java 里只有一个 `String`（再加上可变的 `StringBuilder`），你不需要在两个类型之间做选择。Rust 这里硬是把"字符串数据本身"和"对字符串数据的借用"分成了两个类型。

我们先看几个例子建立感觉：

```rust
let s1: &str = "hello";                  // 字符串字面量是 &'static str
let s2: String = String::from("hello");  // 拥有所有权的 String
let s3: &str = &s2;                      // 从 String 借出 &str
let s4: &str = &s2[0..3];                // 切片，"hel"
```

字符串字面量 `"hello"` 的类型是 `&'static str`，它是写死在程序二进制里的一段 UTF-8 数据，整个程序运行期间都存在（这就是 `'static` 生命周期，后面讲生命周期的时候我们再仔细聊）。

为什么 Rust 要把字符串拆成两个类型？其实回想一下我们前面讲的所有权，这个事情就比较好理解了：

- 字面量是写死在二进制里的，没人"拥有"它，自然只能借用
- 你自己 new 出来的字符串，得有个 owner 来负责释放，所以是 `String`
- 函数参数大部分时候只是想读字符串内容，没必要拿走所有权，那就用 `&str`

第三点特别重要。我们看这两个写法：

```rust
fn greet(name: String) { /* ... */ }    // 拿走所有权，调用方传完就不能用了
fn greet(name: &str)   { /* ... */ }    // 借用，调用方还能继续用
```

绝大多数时候我们应该写成 `&str`，因为它兼容性最好，传 `&String` 会被自动转成 `&str`，传字面量 `"abc"` 也行。这是 Rust 一个非常通用的 API 设计习惯：入参用 `&str`，返回值用 `String`。

类型转换我们看一下，主要也就这几种：

```rust
let s: String = "hello".to_string();    // &str -> String
let s: String = String::from("hello");  // 同上
let s: String = "hello".to_owned();     // 同上，更通用一点的写法

let r: &str = &s;                       // String -> &str，自动 deref
let r: &str = s.as_str();               // 同上，显式写法
```

`to_string`、`to_owned`、`String::from` 三种写法效果都一样，看个人习惯。

接下来看 String 的常用操作：

```rust
let mut s = String::from("hello");
s.push_str(", world");      // 追加 &str
s.push('!');                 // 追加单个 char
s += " 又来";                // 重载了 += 运算符

let s2 = format!("{} - {}", s, 42); // 类似 Java 的 String.format，最常用的拼接方式
```

`format!` 是 Rust 里最常用的字符串格式化方式，跟 `println!` 用法一样，只不过返回 String 而不是打印。

下面讲一个跟 Java 差异很大的点：UTF-8。

Rust 的 String 在底层就是一个 `Vec<u8>`，存的是 UTF-8 编码的字节。这跟 Java 的 String 不一样，Java 的 String 早期是 UTF-16，JDK 9 之后引入了 Compact Strings（ASCII 部分用 byte 存），但对外暴露的 char API 还是 UTF-16 code unit。

这个差异带来一个大坑：Rust 的 String 不能用整数下标访问字符。

```rust
let s = String::from("你好");
let c = s[0];   // 编译错误! String 没有实现 Index<usize>
```

为啥不让索引？因为 UTF-8 是变长编码，中文字符"你"占 3 个字节，如果允许 `s[0]`，那拿到的是第一个字节（不完整字符），这种 API 太容易出错，Rust 干脆不提供。

要遍历字符，用 `chars()`：

```rust
let s = String::from("你好 hello");
for c in s.chars() {
    println!("{c}");    // 一次一个 char，Rust 的 char 是 4 字节 Unicode 标量值
}

for b in s.bytes() {
    println!("{b}");    // 一次一个字节
}
```

要切片，可以按字节范围切，但范围必须落在字符边界上，否则运行时 panic：

```rust
let s = String::from("你好");
let slice = &s[0..3];   // OK，"你" 占 3 个字节
let slice = &s[0..1];   // 运行时 panic! 1 不是字符边界
```

写多了就习惯了，简单说就是：需要按字符处理就用 `chars()`，需要按字节处理就用 `bytes()`，少直接用下标切。如果非得按字符位置切，可以用 `s.char_indices()` 拿到每个字符对应的字节起点。

Rust 的字符串 API 比 Java 啰嗦很多，但安全性和明确性也好很多，这是它一贯的取舍。

最后多说一句：`String` 和 `&str` 这种区分，其实是 Rust 设计的一个缩影，体现了"所有权 vs 借用"对 API 设计的影响。这种"同一个东西分成 owned 和 borrowed 两个类型"的模式，在标准库里到处都是，比如 `PathBuf` / `&Path`、`Vec<T>` / `&[T]`、`OsString` / `&OsStr` 等等。看到这种结对的类型，知道就是这个道理就行。

## 十一、泛型与 Trait：Rust 的抽象机制

讲完字符串，我们终于要面对前面欠了一路的债：trait。Copy 是 trait、Debug 是 trait、Display 也是 trait，`#[derive(Debug)]` 干的事就是自动给你的 struct 实现 Debug 这个 trait。这一节我们把这些零碎认知串起来，而且 trait 离不开泛型，所以我们一起讲。

### 泛型函数：和 Java 长得几乎一样

我们先看泛型函数。Java 里写一个返回数组第一个元素的方法：

```java
public <T> T first(T[] arr) {
    return arr[0];
}
```

Rust 里：

```rust
fn first<T>(list: &[T]) -> &T {
    &list[0]
}
```

`<T>` 这个写法跟 Java 一模一样，T 是类型参数，调用时编译器会做类型推导。注意 Rust 这里返回的是 `&T`，因为我们只是想看一眼，不想拿走所有权（前面所有权那一节讲过的逻辑）。

### 泛型 struct 和 enum

struct 也可以是泛型的：

```rust
struct Point<T> {
    x: T,
    y: T,
}

let int_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
```

不同字段还可以用不同的泛型参数：

```rust
struct Pair<T, U> {
    first: T,
    second: U,
}
```

enum 也一样，前面我们见过的 `Option<T>` 和 `Result<T, E>` 就是泛型 enum：

```rust
enum Option<T> {
    None,
    Some(T),
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}
```

是不是看起来都很眼熟？这部分跟 Java 思路完全一致。

### 单态化：Rust 泛型和 Java 泛型的本质差别

但这里要专门说一下 Rust 泛型在底层的实现，这是 Rust 跟 Java 一个很本质的差别。

Java 的泛型大家都知道，是类型擦除：编译期帮你做类型检查，编译完了泛型信息就没了，所以说 Java 的泛型更像是语法糖，运行时统一是 Object。这就是为什么 Java 里你不能 `new T()`、不能 `T.class`，泛型方法也没法重载只在泛型参数上不同的版本。

Rust 不是这样，Rust 用的叫**单态化**（monomorphization）：编译期看到你用了哪些具体类型，就给每种类型生成一份代码。比如：

```rust
let a = Some(5_i32);
let b = Some("hello");
```

编译之后，相当于编译器帮你生成了两份代码：

```rust
// 大致是这个意思，伪代码
enum Option_i32 { None, Some(i32) }
enum Option_str { None, Some(&'static str) }
```

这就是为什么 Rust 泛型在运行时没有任何性能损失，它编译完根本就没有泛型，全是具体类型，跟你手写一份一份的代码效果一样。代价是二进制可能会变大（每个具体类型一份代码），但运行时是真的快。

Java 的泛型擦除是为了向后兼容，JDK 5 引入泛型时不能破坏已有的字节码。Rust 没有这个历史包袱，从一开始就是单态化。

Rust 的泛型使用上和 Java 几乎没什么差别，还是非常简单的。

### Trait：Rust 的"接口"

trait 你可以先粗暴地理解成 Java 的 interface，它定义一组行为，类型可以选择实现这些行为：

```rust
trait Greet {
    fn hello(&self);
}

struct English;
struct Chinese;

impl Greet for English {
    fn hello(&self) {
        println!("Hello!");
    }
}

impl Greet for Chinese {
    fn hello(&self) {
        println!("你好！");
    }
}
```

这跟 Java 的 interface 思路上几乎一样，只不过语法上有个非常关键的差别：

- Java 里实现 interface 是写在类定义那里的（`class English implements Greet`）
- Rust 里实现 trait 是单独写一个 `impl Trait for Type` 块，跟类型定义是分开的

这个差别很重要，因为它意味着你可以给已经存在的类型实现新的 trait。比如你想给 `i32` 加点新方法：

```rust
trait Double {
    fn double(&self) -> Self;
}

impl Double for i32 {
    fn double(&self) -> Self {
        self * 2
    }
}

let x = 5_i32;
let y = x.double();   // 10
```

是不是很爽？Java 里要做这种事只能写工具类的 static 方法，永远没法写成 `5.double()` 这种调用形式。Kotlin 里的扩展函数其实就是借鉴自这种模式。

### 默认方法

跟 Java 8 之后的 default method 一样，trait 里也可以提供默认实现：

```rust
trait Greet {
    fn hello(&self) {
        println!("Hi there!");   // 默认实现
    }
}

struct Anonymous;
impl Greet for Anonymous {}    // 啥也不写，用默认实现

Anonymous.hello();   // 输出 Hi there!
```

实现的时候不写就用默认的，写了就覆盖默认实现，跟 Java 一模一样。

### 孤儿规则

上面我们说"可以给已有类型实现新 trait"，但这里有个很重要的限制，叫**孤儿规则**（orphan rule）：

> trait 或者类型，必须有一个是你自己 crate 里定义的，你才能写 `impl Trait for Type`。

也就是说：

- 可以给你自己的 struct 实现别人的 trait（比如 Display）
- 可以给别人的类型实现你自己的 trait
- 不可以给别人的类型实现别人的 trait

为什么要这么限制？想象一下，如果你能给 `Vec<T>` 实现 `Display`，我也能给 `Vec<T>` 实现 `Display`，那两份代码同时存在就冲突了，编译器也不知道用谁的。孤儿规则就是为了避免这种冲突。

绕过孤儿规则的常见做法是用 newtype 模式包一层：

```rust
struct MyVec(Vec<i32>);   // 用一个 tuple struct 包一下，这就是我自己的类型了

impl std::fmt::Display for MyVec {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        // ...
        Ok(())
    }
}
```

### Trait Bound：泛型 + trait

讲到这里，我们可以把泛型和 trait 串起来了，其实对于 Java 开发者来说，这也不是什么新鲜东西。

我们前面写过这个函数：

```rust
fn first<T>(list: &[T]) -> &T {
    &list[0]
}
```

这个能编译。但如果我们想找最大值呢？

```rust
fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {    // 编译错误！T 未必能比较
            largest = item;
        }
    }
    largest
}
```

这就编译不过了。因为 T 是任意类型，编译器不知道它能不能用 `>` 比较。

我们得告诉编译器：T 必须实现某个能比较大小的 trait。这就是 trait bound：

```rust
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    // ...
}
```

`T: PartialOrd` 的意思是"T 必须实现 PartialOrd 这个 trait"。`PartialOrd` 是标准库里定义"能不能比较大小"的 trait。

这个跟 Java 里的 `<T extends Comparable<T>>` 是一个意思，只不过 Rust 用 `:` 不用 `extends`。

多个 bound 用 `+` 连接：

```rust
use std::fmt::Display;

fn print_and_compare<T: PartialOrd + Display>(a: T, b: T) {
    // T 必须既能比较，又能用 Display 打印
}
```

bound 一多，写在 `<>` 里太挤，可以用 `where` 子句拉到后面：

```rust
fn complex<T, U>(a: T, b: U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // ...
}
```

`where` 子句完全等价于写在 `<>` 里的形式，纯粹是为了可读性，两种代码我们都很常见。

### 标准库里常见的 trait

Rust 标准库里有一堆"基础设施"级别的 trait，写代码经常会碰到。我们挑最常见的过一下：

- `Debug`：调试用打印，对应 `{:?}`，前面 struct 那节讲过
- `Display`：用户可见的打印，对应 `{}`，需要手动实现
- `Clone`：深拷贝，对应 `.clone()` 方法
- `Copy`：按位复制，前面所有权那节讲过
- `PartialEq` / `Eq`：相等比较，对应 `==`
- `PartialOrd` / `Ord`：大小比较，对应 `<` `>`
- `Default`：提供默认值，调用 `T::default()`，这里的默认值类似 Java 中的零值
- `Hash`：哈希计算，作为 HashMap 的 key 必须实现
- `From` / `Into`：类型转换，下面单独说

为什么 `PartialEq` 和 `Eq` 要分两个？因为浮点数有 NaN，`NaN != NaN`，没法满足完整等价关系，所以浮点数只实现 `PartialEq` 不实现 `Eq`。`PartialOrd` 和 `Ord` 同理，浮点数只实现前者。这种"完整版和残缺版"的成对设计在 Rust 里很常见，反映的是数学意义上严格不严格的差别。

这些 trait 大部分可以用 `#[derive(...)]` 自动派生：

```rust
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
    name: String,
    age: u32,
}
```

这相当于 Java 里 Lombok 的 `@Data` 之类的注解。一个 struct 上挂一串 derive 是非常常见的写法。

### From 和 Into

`From` 和 `Into` 这一对 trait 用得特别多，专门讲一下，它们做类型转换，类似 Java 中的 Converter，把一个对象转换成另一个对象：

```rust
let s: String = String::from("hello");   // 用 From
let s: String = "hello".into();          // 用 Into
```

这两个写法效果一样。规则是：你只要给类型实现了 `From`，编译器自动给你来一份对应的 `Into`，不用自己写：

```rust
impl From<&str> for String { /* 标准库实现 */ }

// 编译器自动推导出：
// impl Into<String> for &str { /* ... */ }
```

所以写代码的时候，实现就实现 From，调用就尽量用 Into，灵活性最高。

`?` 操作符里也用了 `From`，做错误类型自动转换。比如你的函数返回 `Result<T, MyError>`，里面调用了一个返回 `Result<T, io::Error>` 的函数，只要 `MyError` 实现了 `From<io::Error>`，`?` 就能自动帮你把 `io::Error` 转成 `MyError`：

```rust
impl From<io::Error> for MyError {
    fn from(e: io::Error) -> Self { /* ... */ }
}

fn read() -> Result<String, MyError> {
    let s = std::fs::read_to_string("a.txt")?;   // io::Error 自动转成 MyError
    Ok(s)
}
```

这个特性让自定义错误类型用起来非常顺手，是 Rust 错误处理生态非常重要的一块。日常项目里大家一般会用 `thiserror` 这个库帮你自动生成这些 `From` 实现，更省事，这里先不展开。

### 静态分发 vs 动态分发：impl Trait 和 dyn Trait

最后我们来聊一个 Rust 跟 Java 差别非常大的点：多态怎么实现。

Java 里多态就一种方式，运行时动态分发。你写 `List<String>`，运行时根据具体类型（ArrayList 还是 LinkedList）查虚表（vtable）找方法，这个开销虽然小，但是有的。

Rust 提供了两种方式，写起来不一样，运行时行为也不一样。

第一种，`impl Trait`，**静态分发**：

```rust
fn greet(g: impl Greet) {
    g.hello();
}
```

这里参数类型写的是 `impl Greet`，意思是"任何实现了 Greet 的类型"。编译时编译器看到你传了 `English`，就给你生成一份 `greet_for_English` 的代码；传了 `Chinese`，再生成一份 `greet_for_Chinese`。这就是单态化的应用，运行时没有查表，没有性能损失。

 `impl Trait` 等价于泛型，对于 Java 开发者来说，会更喜欢用泛型，下面这两个写法效果是一样的：

```rust
fn greet(g: impl Greet) { /* ... */ }
fn greet<T: Greet>(g: T) { /* ... */ }
```

第二种，`dyn Trait`，**动态分发**：

```rust
fn greet(g: &dyn Greet) {
    g.hello();
}
```

`dyn Greet` 表示"某个实现了 Greet 的类型，但具体是啥到运行时才知道"。这个就跟 Java 的 interface 引用就很像了，运行时通过虚表找方法。

为什么前面要加 `&`？因为 `dyn Trait` 是 unsized 的，不同具体类型大小不一样，编译器没法在栈上给它分配固定大小，只能通过指针来访问。这点 Java 程序员可能不太直观，因为 Java 里所有对象引用大小都一样（都是 8 字节的引用），不存在这个问题。

后面讲智能指针那一节我们会看到，除了 `&`，还有别的指针类型也能包住 `dyn Trait`，原理一样。这里先用最简单的 `&` 就够了。

什么时候用哪个？经验法则：

- 同质集合（一个 Vec 里的元素都是同一个具体类型）→ 用 `impl Trait` / 泛型
- 异质集合（一个 Vec 里要装不同的具体类型）→ 用 `&dyn Trait`

举个例子：

```rust
// 异质集合：必须用 dyn
let english = English;
let chinese = Chinese;
let greeters: Vec<&dyn Greet> = vec![&english, &chinese];
for g in &greeters {
    g.hello();
}
```

这种情况就只能用 dyn，因为 Vec 要求所有元素大小一致，但是这里要往 Vec 里面塞两个不同的类型，使用泛型的版本肯定编译不过。

简单总结：默认用 `impl Trait` / 泛型，性能最好，这也是大多数情况；只有当你确实需要"在一个集合里装不同具体类型"的时候，才退而求其次用 `dyn Trait`。

### 关联类型

最后再补充一个挺重要的概念：关联类型（associated type）。

关联类型说白了就是 trait 里面声明的一个"类型占位符"，让实现这个 trait 的类型自己来填。最经典的例子就是 Iterator：

```rust
pub trait Iterator {
    type Item;   // 关联类型，每个迭代器自己说"我吐出来的元素是啥类型"

    fn next(&mut self) -> Option<Self::Item>;
}
```

这里的 `type Item` 就是关联类型。每个实现 Iterator 的类型，自己定下 `Item` 是啥：

```rust
struct Counter { count: u32 }

impl Iterator for Counter {
    type Item = u32;   // 我这个迭代器吐的是 u32

    fn next(&mut self) -> Option<u32> {
        self.count += 1;
        Some(self.count)
    }
}
```

读到这里大家可能会犯嘀咕：这跟泛型有啥区别？我直接写成 `trait Iterator<T>` 不也一样吗？

我们拿标准库里两个 trait 对比一下就清楚了。`From` 用的是泛型参数，`Iterator` 用的是关联类型，为啥选了不同的方式？

先看 From：

```rust
pub trait From<T> {
    fn from(value: T) -> Self;
}
```

为啥 From 用泛型？因为同一个类型可以从很多种其他类型转换过来。比如 `String`，很多其他的类型都可以转换成 String：

```rust
impl From<&str> for String { /* ... */ }
impl From<char> for String { /* ... */ }
```

注意，是同一个 `String`，给 From 实现了多次，每次 T 不一样。这种"一个类型对一个 trait 可以多次实现"，正是泛型参数的特点。这里你可以理解为，`From<&str>` 和 `From<char>` 在编译器眼里就是两个不同的 trait，所以同一个类型分别实现这两个 trait 是没问题的。

再看 Iterator。反过来想一下，假设硬把它写成 `trait Iterator<T>`，那就允许给 `Counter` 同时写 `impl Iterator<u32>` 和 `impl Iterator<String>` 两份实现，会发生什么？

```rust
let mut c = Counter { count: 0 };
c.next();   // 编译器懵了：你要哪个 next？u32 那个还是 String 那个？
```

调用方就得每次显式标类型参数才能消歧义，非常难受。但其实仔细想想，"一个迭代器同时能吐 u32 又能吐 String"这种事根本不符合迭代器的语义。一个具体的迭代器，元素类型就该是固定的，只该实现一次。关联类型就是把"每个实现者只能填一种类型"这个约束写进了 trait 定义里，从源头避免了上面那种歧义。

所以选哪种，看的就是这个：

- 同一个类型可以"自然地"实现这个 trait 多次（每次类型参数不同）→ 用泛型参数，代表：`From<T>`、`Add<Rhs>`
- 同一个类型对这个 trait 只该有一种实现 → 用关联类型，代表：`Iterator`、`Deref`

调用方约束关联类型的语法长这样：

```rust
fn sum<I: Iterator<Item = i32>>(iter: I) -> i32 { /* ... */ }
```

`Item = i32` 意思是"我要的迭代器，它的 Item 必须是 i32"。日常代码里这种写法见得非常多。

先把这个概念建立起来就够了，剩下的等真用到再深究。

到这里 trait 和泛型的核心内容就讲完了。我们还跳过了一些东西，比如 trait 对象的安全性（object safety）等，留给读者自己慢慢探索。这一节的目标是先建立起整体认知，能看懂代码、能写常见的代码就够了。

## 十二、生命周期：让借用规则自洽的最后一块拼图

前面所有权那一节我们讲过引用：`&s` 把值借出去，所有者还在外面。当时为了让大家先建立直觉，我们故意没提一个东西：生命周期（lifetime）。这一节就是要把这个坑填上。

生命周期可能是 Rust 里最让人懵的概念之一，但它要解决的问题其实很简单，甚至大部分时候我们可以不用关心。

### 为什么需要生命周期？

先看一段代码：

```rust
fn main() {
    let r;                  // 1. 声明 r
    {
        let x = 5;
        r = &x;             // 2. 让 r 指向 x
    }                       // 3. x 在这里被销毁
    println!("r: {r}");     // 4. 用 r —— 但 x 已经不存在了！
}
```

这段代码在 C/C++ 里会编译过去，运行时拿到一个悬垂指针，行为就完全不可预期了。在 Java 里这个问题不存在，因为只要还有引用指向 x，GC 就不会回收 x。

Rust 既没有 GC，又允许引用，怎么办？答案就是生命周期。

Rust 编译器有个组件叫**借用检查器**（borrow checker），它的工作就是检查每个引用的"存活期"是不是落在被引用对象的"存活期"以内。上面这段代码，借用检查器一看就发现：`r` 想活到 `println!` 那行，但它指向的 `x` 在内层大括号结束就死了。这种引用 Rust 是不允许存在的，编译器直接报错。

所以记住一个核心原则：

> 引用的存活时间不能超过它所指向的值。

这个原则在大部分简单代码里编译器自己就能推导出来，我们感觉不到生命周期的存在。但有些场景编译器推导不出来，就需要我们手动标注。

### 生命周期标注的基本语法

生命周期标注用一个撇号加一个名字，比如 `'a`、`'b`、`'static`：

```rust
&i32        // 普通引用
&'a i32     // 带生命周期标注的引用，表示这个引用活在 'a 这个范围里
&'a mut i32 // 带生命周期标注的可变引用
```

`'a` 这个名字本身没啥含义，就跟泛型参数 T 一样，是个占位符。读的时候 `'a` 念作 "tick a"。

光看语法没啥意义，我们看它在哪儿用。

### 函数签名里的生命周期

经典例子：写一个函数，返回两个字符串切片中比较长的那个。

```rust
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
```

这段代码看起来人畜无害，但是编译不过：

```
error[E0106]: missing lifetime specifier
```

为啥？因为编译器需要知道：返回的 `&str` 是一个借用，但是到底是借自 x 还是借自 y？这两个的生命周期可能不一样，调用方收到这个返回值，能用多久，编译器算不出来。

我们来标注：

```rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
```

这段标注的意思是：x、y 和返回值这三者共享同一个生命周期 `'a`。具体说就是：返回值的生命周期，是 x 和 y 中较短的那个。

这个怎么用呢？看例子：

```rust
let s1 = String::from("long string");
let result;
{
    let s2 = String::from("short");
    result = longest(s1.as_str(), s2.as_str());
    println!("{result}");   // 在这里用 result，s2 还活着，OK
}
// println!("{result}");   // 这里就不行了，s2 已经死了，编译错误
```

注意一点：生命周期标注不会真的改变值的存活时间，它只是给编译器的描述，告诉编译器"这几个引用应该满足什么样的关系"。如果你描述的关系不成立，编译器会报错；但加了标注本身不会让对象多活一会儿。

类比 Java：Java 的泛型 `<T>` 是描述类型的关系，不会真的造出新类型来。Rust 的 `<'a>` 也是同样的角色，描述生命周期之间的关系。

### Struct 里的引用：必须标注生命周期

Struct 那一节我们说过，如果 struct 字段是引用，必须标注生命周期，当时跳过了。现在补上：

```rust
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);
}
```

`Excerpt<'a>` 的意思是：这个 struct 持有一个引用，引用的生命周期是 `'a`，而 struct 实例本身的存活时间不能超过 `'a`。说白了就是，excerpt 不能比 novel 活得久。

为啥要这么搞？因为 struct 一旦有了引用字段，它就不再是独立的"自包含"对象，它依赖外部的数据存活。Rust 必须知道这种依赖关系，才能保证安全。

实际上，初学的时候大部分情况下你都不应该让 struct 持有引用，让 struct 拥有 `String` 而不是 `&str`、拥有 `Vec<T>` 而不是 `&[T]`，写起来简单很多。只有性能敏感、确实需要避免拷贝的时候，才考虑用引用 + 生命周期。

### 生命周期省略规则

看到这里读者可能会嘀咕：那为啥前面那么多代码都没写生命周期，也都能编译？比如：

```rust
fn first_word(s: &str) -> &str {
    // ...
    s
}
```

这个返回了一个引用，按道理需要标注啊。

答案是：Rust 有一套生命周期省略规则（lifetime elision rules），有些常见的情况编译器自己能推出来，就不需要你写了。规则一共三条：

1. 每个引用参数都有自己的生命周期参数。比如 `fn f(x: &i32, y: &i32)` 实际等价于 `fn f<'a, 'b>(x: &'a i32, y: &'b i32)`
2. 如果只有一个输入生命周期，那它就赋给所有输出生命周期
3. 如果有多个输入生命周期，但其中有 `&self` 或 `&mut self`，那 self 的生命周期赋给所有输出生命周期

第二条覆盖了大部分常见情况。比如 `fn first_word(s: &str) -> &str`，输入只有一个引用，输出的生命周期就跟输入一样，编译器自己推出来。

第三条是为实例方法服务的。Rust 觉得"方法返回的引用，大概率是借自 self"，所以默认就这么推。

如果不符合这三条规则（比如前面 `longest` 那种"两个引用参数都不是 self、编译器没法决定返回值借自哪个"的情况），编译器就会报错让你手动标。

实际写代码的时候，刚开始报这种错很正常，看错误提示加上对应的 `<'a>` 就行。写多了就有感觉了。

### 'static 生命周期

有一个特殊的生命周期叫 `'static`，表示"活到程序结束"。前面字符串那节我们提过，字符串字面量的类型就是 `&'static str`，因为它们写死在二进制里，整个程序运行期间都存在：

```rust
let s: &'static str = "I have a static lifetime.";
```

`'static` 听起来很美好，活得最久，谁都能用。但要小心，不要看到编译器提示 `&'static` 就无脑加上去，那通常是把问题藏起来了，不是真的解决问题。`'static` 应该用在那些你确信"这玩意儿真的会一直活到程序结束"的地方，比如全局常量、配置数据、`Box::leak` 出来的内存。

另外，`'static` 还有第二个含义，是作为 trait bound 用的：`T: 'static` 表示"T 不包含任何生命周期短于 'static 的引用"。这个含义在多线程那块会反复出现（因为线程可能存活到程序结束），先不展开，知道有这么个东西就行。

### 啥时候真的需要关心生命周期？

讲了这么多，最后给个实用的判断：

- 写普通业务代码、struct 不持有引用 → 几乎用不到生命周期标注
- 写函数返回引用、且参数有多个引用 → 可能要标
- 写 struct 持有引用 → 必须要标
- 写库代码、追求零拷贝 → 频繁要标
- 写异步代码 → 经常被生命周期问题坑（async 那块情况复杂，后面讲）

新手项目里，遇到生命周期问题最简单的解决方式往往是"换成拥有的类型"，比如`&str` 改成 `String`，`&[T]` 改成 `Vec<T>`。性能可能差一点点，但代码简单很多。等你对所有权和借用有了肌肉记忆，再去考虑零拷贝优化也来得及。

Rust 的生命周期是它独有的设计，刚开始确实费脑子。但好消息是：编译器报错信息一般都很清晰，能告诉你哪两个生命周期对不上，按提示改基本能改对。习惯就好。

到这里 Rust 类型系统的核心三件套：泛型、trait、生命周期，现在就都讲完了。其实也没那么复杂，对于 Java 开发者来说，泛型非常简单，几乎和 Java 差不多，trait 和接口其实也差不了多少，只不过功能强大一些，lifetime 算是一个新东西，但是用的地方其实不多。

## 十三、闭包和迭代器：跟 Java Stream 思路一致，但更深入

Java 程序员对这两个东西其实不陌生：Java 8 引入的 lambda 和 Stream API 思路上跟它们高度一致。所以这一节的学习曲线不陡，主要是了解 Rust 在所有权约束下的一些细节差异。

### 闭包：跟 Java lambda 几乎一样

闭包语法:

```rust
let add = |a: i32, b: i32| -> i32 { a + b };
let result = add(1, 2);   // 3
```

写法是 `|参数| 表达式` 或者 `|参数| { 多行 }`。跟 Java 的 `(a, b) -> a + b` 思路一样，只不过参数列表用 `||` 包起来。

类型一般可以省略：

```rust
let add = |a, b| a + b;
let r = add(1_i32, 2);    // OK，编译器从第一次调用推出 a, b 是 i32
```

但要注意，闭包的参数和返回类型一旦推出来就固定了，不能像泛型函数那样多次用不同类型调用：

```rust
let add = |a, b| a + b;
let r1 = add(1_i32, 2);
let r2 = add(1.0_f64, 2.0);   // 编译错误！add 的参数类型已经定为 i32 了
```

可能大家看到这里会觉得有点奇怪，但是如果你了解了闭包在经过编译器转换以后其实就是一个结构体，可能就知道为什么了。

闭包最大的特点是捕获环境。Java 的 lambda 也能捕获外部变量，但只能捕获 final 或 effectively final 的：

```java
int x = 10;
Runnable r = () -> System.out.println(x);   // OK，x 是 effectively final
// x = 11;   // 这样就不行，因为 r 里捕获了 x
```

Rust 的闭包能捕获各种形式，分得比 Java 更细。

### 三种 Fn trait：闭包的三种"档位"

Rust 没有 Java 那种"functional interface"的概念，闭包是怎么搞的？答案是 trait。Rust 标准库里有三个 trait 用来表示闭包，按"对环境的占用程度"分三档：

- `FnOnce`：只能调用一次（会消费掉捕获的值）
- `FnMut`：可以多次调用，并且会修改捕获的值
- `Fn`：可以多次调用，只读捕获

这三个是层层包含的关系：能 `Fn` 的肯定能 `FnMut` 和 `FnOnce`，能 `FnMut` 的肯定能 `FnOnce`。

什么样的闭包属于哪一档？编译器会根据闭包对捕获变量做了什么自动推：

```rust
// 只读捕获 → Fn
let s = String::from("hello");
let print_s = || println!("{s}");
print_s();
print_s();   // 多次调用没问题

// 修改捕获 → FnMut
let mut s = String::from("hello");
let mut append = || s.push_str(" world");
append();
println!("{s}");   // "hello world"

// 移动并消费捕获 → FnOnce
let s = String::from("hello");
let consume = move || { let _moved = s; };  // s 被移到 _moved 里，闭包用一次就废了
consume();
// consume();   // 编译错误：FnOnce 只能调用一次
```

这三档对应的就是前面所有权那节讲的三种引用方式：`&self`、`&mut self`、`self`。是不是又串起来了？Rust 的概念有种很爽的"环环相扣"感。

### move 关键字：强制拿走所有权

默认情况下闭包用最弱的捕获方式：能用 `&` 就用 `&`，需要改才用 `&mut`，需要消费才 move。但有时候我们想强制让闭包拿走所有权，特别是把闭包传到另一个线程里：

```rust
use std::thread;

let s = String::from("hello");

let handle = thread::spawn(move || {   // move 强制把 s 移进闭包里
    println!("from thread: {s}");
});

handle.join().unwrap();
```

为啥要 move？因为子线程可能比 `s` 所在的作用域活得更久，如果还是借用方式，`s` 一被释放子线程就拿到悬垂引用了。`move` 让闭包把 `s` 整个拿走，所有权转移到子线程那边，安全。

这个东西后面讲并发的时候还会反复见，先有印象就行。

### 迭代器：跟 Java Stream 思路一样

接下来是迭代器。Java 程序员对 Stream API 已经很熟了：

```java
int sum = list.stream()
              .filter(x -> x > 2)
              .mapToInt(x -> x * 2)
              .sum();
```

Rust 写起来差不多：

```rust
let sum: i32 = v.iter()
                .filter(|&&x| x > 2)
                .map(|x| x * 2)
                .sum();
```

迭代器的核心是 `Iterator` trait，简化版长这样：

```rust
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // 还有一大堆默认方法：map, filter, fold, sum, collect 等等
}
```

只要给一个类型实现 `next` 方法，剩下的几十个组合都白送，这是 trait 默认方法的威力。

### iter / iter_mut / into_iter：三种迭代方式

集合那节我们提过这三个方法，这里展开说一下。它们的区别就一句话：拿什么形式的引用：

```rust
let v = vec![1, 2, 3];

v.iter();        // 产生 &T，只读借用
v.iter_mut();    // 产生 &mut T，可变借用，可以修改元素
v.into_iter();   // 产生 T，拿走所有权，迭代完原集合就没了
```

`for x in v` 这个语法糖等价于 `v.into_iter()`，所以默认会拿走所有权。这就是为什么集合那节我们写：

```rust
for x in &v {        // 显式借用，等价于 v.iter()
    println!("{x}");
}

for x in &mut v {    // 等价于 v.iter_mut()
    *x += 10;
}

for x in v {         // 等价于 v.into_iter()，迭代完 v 就用不了
    println!("{x}");
}
```

接下来，我们来解释这行代码：

```rust
let sum: i32 = v.iter().filter(|&&x| x > 2).map(|x| x * 2).sum();
```

`v.iter()` 产生的是 `&i32`（不是 `i32`），这很好理解，这个方法返回的是只读借用，filter 接收的闭包参数类型是 `&Item`，所以 filter 那个闭包拿到的是 `&&i32`：

```rust
v.iter()                         // 迭代器产生 &i32
 .filter(|x| /* x: &&i32 */ )    // filter 拿到的是 &Item
 .map(|x|    /* x: &i32  */ )    // map 拿到的是 Item
 .sum()
```

那 `|&&x|` 是啥？是模式匹配解构：

- 第一个 `&` 解掉外层的 `&&i32` → `&i32`
- 第二个 `&` 解掉内层的 `&i32` → `i32`

最后 `x` 就是 `i32`，可以直接 `x > 2` 比较，写起来更顺。

如果你觉得 `|&&x|` 反人类，写成下面这样也对：

```rust
v.iter().filter(|x| **x > 2)         // 不解构，调用时手动解引用
v.iter().filter(|&&x| x > 2)         // 解构，写法上看起来怪一点
v.iter().copied().filter(|&x| x > 2) // 先 copy 一层，让迭代器产生 i32 而不是 &i32
```

一般大家都是怎么顺怎么来，一时不理解也没关系，记住就行了。Rust 编译器对引用和解引用的容忍度很高，多种写法都能编过。

### 惰性求值

跟 Java Stream 一样，Rust 的迭代器也是惰性的：你写一串 `.map(...).filter(...)` 啥都不会发生，只有调用了消费方法才会真的执行：

```rust
let v = vec![1, 2, 3];
let it = v.iter().map(|x| {
    println!("processing {x}");
    x * 2
});
// 到这里啥都不打印

for x in it {
    println!("got {x}");
}
// 这时候才开始打印 processing 1, got 2, processing 2, got 4...
```

注意 Rust 是真正的"逐个产生"，上面这段代码，`map` 不会一次性把整个 vec 处理完再给下面的 `for`代码块，而是 for 每问一个，map 才算一个。这跟 Java Stream 也一样。

常见的消费方法：

```rust
.collect()    // 收集到一个集合里，类似 Java 的 .collect(Collectors.toList())
.sum()        // 求和
.count()      // 计数
.fold(...)    // 类似 Java 的 reduce
.for_each(...)// 副作用迭代
.find(...)    // 找第一个满足条件的，返回 Option
.any(...)     // 是否有满足条件的，返回 bool
.all(...)     // 是否所有都满足
```

`collect` 这个特别提一下，它需要你告诉它收到什么类型里：

```rust
use std::collections::HashSet;

let v: Vec<i32> = (1..=10).filter(|x| *x % 2 == 0).collect();      // 收到 Vec
let s: HashSet<i32> = (1..=10).filter(|x| *x % 2 == 0).collect();  // 收到 HashSet
let s: String = ['h', 'i'].into_iter().collect();                  // 收到 String
```

类型由声明决定，编译器根据目标类型反推 collect 该怎么收。这是 Rust 类型推导的一个强大用法。如果不想写在变量上，也可以这样传：`.collect::<Vec<_>>()`，效果一样。

### 零成本抽象

说到迭代器，一定要提一句 Rust 的"零成本抽象"。Java Stream 用起来爽，但要付出一些代价：装箱拆箱、虚函数调用、临时对象分配等等。Rust 的迭代器经过单态化和内联优化，编译出来的代码跟手写循环几乎一模一样：

```rust
// 这个高级写法
let sum: i32 = (1..=100).filter(|x| *x % 2 == 0).sum();

// 编译之后的效率，约等于这个手写版本
let mut sum = 0_i32;
for i in 1..=100 {
    if i % 2 == 0 {
        sum += i;
    }
}
```

所以在 Rust 里你可以放心大胆用迭代器链式调用，不用担心性能。Java 那边对性能敏感的代码，有时候还是会回退到 for 循环，Rust 这边没这个顾虑。

### 自定义迭代器

最后看一眼怎么自己实现一个迭代器。其实就是给类型实现 `Iterator` trait，提供 `next` 方法：

```rust
struct Counter {
    count: u32,
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<u32> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None    // 返回 None 就表示迭代结束
        }
    }
}

let c = Counter { count: 0 };
for x in c {
    println!("{x}");   // 1, 2, 3, 4, 5
}

// 实现了 Iterator 之后，map/filter/sum 这些方法全都自动可用
let total: u32 = Counter { count: 0 }.sum();
```

跟 Java 的 `Iterator` 接口接近，只不过 Java 的接口里 `hasNext` 和 `next` 是分开的，Rust 把"还有没有"和"取下一个"打包到 `Option` 里返回，更紧凑。这种"用 Option 表达可能没有"的设计在 Rust 里到处都是，前面 HashMap 的 get 也是这个套路。

到这里闭包和迭代器就讲完了。这一节因为有 Java Stream 打底，应该是比较好理解的一节。

## 十四、智能指针：Rust 里的"特殊引用"

前面我们一直在用普通的引用 `&T` 和 `&mut T`，这些在所有权和借用规则里都很自然。但有些场景普通引用满足不了，比如：

- 数据要放堆上（普通引用不能控制分配位置）
- 同一份数据需要多个所有者（违反"单一所有者"规则）
- 在 `&self` 方法里也想修改某个字段（违反借用规则）

这些场景就要用到智能指针（smart pointer）。Rust 里的智能指针其实就是一些特殊的类型，行为上像指针，但带了额外能力。我们这一节讲最常用的几个：`Box`、`Rc`、`RefCell`、`Arc`、`Mutex`、`Weak`。

讲之前先说一句：智能指针这个名字听起来很高大上，其实没那么玄乎。Rust 的"智能指针"基本上都是普通的 struct，里面包了一个指针 + 一些控制逻辑。`String` 严格说也算一种智能指针（它管理一段堆上的 UTF-8 数据）。所以不要被名字吓到。

### Box\<T>：最基础的堆分配

`Box<T>` 是最简单的智能指针，作用就是把 `T` 放到堆上：

```rust
let b = Box::new(5);   // 在堆上分配一个 i32，值为 5
println!("b = {b}");   // 5，自动解引用
```

类比 Java：Java 里所有对象都在堆上，引用在栈上指过去，这是默认行为。Rust 里基础类型默认在栈上，要放堆上就得 `Box::new(...)`。所以可以理解成 `Box::new(x)` 就是 Java 里"new 这个值放堆上"的显式写法，理解了这个，Box 就非常简单了。

`Box<T>` 拥有所有权，离开作用域时会自动释放堆上的内存（GC 的工作 Rust 在编译期就安排好了）：

```rust
{
    let b = Box::new(5);
    // 用 b
}   // 这里 b 离开作用域，堆内存自动释放
```

`Box` 在三种场景里特别有用。

第一种，**递归类型**。这也是官方教程使用的例子，我们如果要写一个链表：

```rust
enum List {
    Cons(i32, List),   // 编译错误：infinite size
    Nil,
}
```

这编译不过，因为编译器要算 `List` 的大小，发现里面又包了一个 `List`，无限递归没法算。用 `Box` 就行：

```rust
enum List {
    Cons(i32, Box<List>),   // Box 的大小是固定的（一个指针的大小）
    Nil,
}
```

`Box<List>` 是一个指针，大小固定，编译器能算出来。这跟 Java 不一样：Java 里链表节点之间天然就是引用关系，根本不会遇到这个问题。Rust 因为默认是嵌入式存储（值直接放进去），所以才需要 Box 来"打断"递归。

第二种，很大的对象，避免 move 时拷贝整个数据。

第三种，`Box<dyn Trait>` 做动态分发，前面 trait 那节我们介绍过这么一行代码：

```rust
let greeters: Vec<&dyn Greet> = vec![&english, &chinese];
```

但其实一般我们会这么用：

```rust
let greeters: Vec<Box<dyn Greet>> = vec![
    Box::new(English),
    Box::new(Chinese),
];
```

### Deref 和 Drop：智能指针之所以"智能"

这里顺便提两个 trait，所有智能指针都会实现。

`Deref` 是解引用，让智能指针可以像普通引用一样用：

```rust
let b = Box::new(5);
let n = *b;   // 解引用，拿到 5。这背后调用的是 Box 的 Deref 实现
```

更常见的是"自动解引用"，你调用方法时编译器会自动帮你解：

```rust
let s = Box::new(String::from("hello"));
println!("{}", s.len());   // 编译器自动解引用，相当于 (*s).len()
```

`Drop` 相当于 Java 的 finalizer，但比 finalizer 靠谱得多，它的执行时机是确定的（变量离开作用域时立即执行），而 Java 的 finalize 啥时候被调用全看 GC 心情：

```rust
struct Logger;
impl Drop for Logger {
    fn drop(&mut self) {
        println!("Logger dropped!");
    }
}

{
    let _l = Logger;
}   // 这里打印 "Logger dropped!"
```

Rust 的 `Drop` 跟 C++ 的析构函数差不多，是 RAII（资源获取即初始化）模式的体现。Java 后来引入的 `try-with-resources` 也是 RAII 思想，但只在 try 块里有效，没法在变量离开作用域时自动触发。

知道有这俩 trait 就行，平时一般不用自己实现。

### Rc\<T>：单线程下的多所有权

Rust 的所有权规则说"一个值只能有一个 owner"。这在大部分场景下没问题，但有些场景就憋屈了，比如图、树、共享数据结构等等，这种数据天然就是多个地方在引用同一份数据。

`Rc<T>`（reference counted）就是用来处理这种情况的。它在内部维护一个引用计数，每次 clone 就 +1，每次 drop 就 -1，计数为 0 时才真正释放数据：

```rust
use std::rc::Rc;

let a = Rc::new(String::from("hello"));
println!("count: {}", Rc::strong_count(&a));   // 1

let b = Rc::clone(&a);
println!("count: {}", Rc::strong_count(&a));   // 2

{
    let c = Rc::clone(&a);
    println!("count: {}", Rc::strong_count(&a));   // 3
}   // c 离开作用域，count 减回 2
```

注意 `Rc::clone(&a)` 跟你想象的"clone 一份数据"不一样，它只是把引用计数 +1，底层数据还是同一份。这点跟 Java 里"复制引用"（多个变量指向同一个对象）思路是一样的。

`Rc::clone` 也可以写成 `a.clone()`，效果相同。社区习惯写 `Rc::clone(&a)` 是为了让"这是引用计数 +1，不是深拷贝"这件事一眼能看出来。

`Rc<T>` 有两个限制必须强调：

1. 只能用在单线程。多线程要用 `Arc<T>`（稍后讲）。
2. 拿到的内容是不可变的。`Rc<T>` 只给你 `&T`，不会给 `&mut T`，因为多个 Rc 指向同一份数据，给可变引用就违反借用规则了。

第二点很要命：很多场景我们就是想要"多个所有者，且能修改"。怎么办？这就要看下面这位了。

### RefCell\<T>：把借用检查推到运行时

`RefCell<T>` 提供的能力叫**内部可变性**（interior mutability）：你拿着 `&T`（不可变引用），但可以"通过它"修改里面的数据。

听起来违反借用规则？是的，但 `RefCell` 在运行时自己检查规则：

```rust
use std::cell::RefCell;

let c = RefCell::new(5);

{
    let r = c.borrow();        // 不可变借用
    println!("{}", *r);
}   // r 离开作用域，借用结束

{
    let mut r = c.borrow_mut();  // 可变借用
    *r += 1;
}

println!("{}", c.borrow());   // 6
```

`borrow()` / `borrow_mut()` 这俩方法在运行时跟踪借用情况，规则跟编译期借用检查一样：要么有任意多个 `borrow()`，要么只有一个 `borrow_mut()`。违反的话直接 panic：

```rust
let c = RefCell::new(5);
let r1 = c.borrow_mut();
let r2 = c.borrow();   // 运行时 panic！already borrowed mutably
```

可能有读者要问：好端端的编译期检查为啥要换成运行时检查？答案是：编译器看不到所有信息。比如下面这种：

```rust
struct MyType {
    cache: RefCell<HashMap<String, String>>,   // cache 是内部状态
}

impl MyType {
    fn get(&self, key: &str) -> String {       // 注意是 &self，不是 &mut self
        let mut cache = self.cache.borrow_mut();
        // 在 &self 方法里修改 cache，靠 RefCell 实现
        // ...
        String::new()
    }
}
```

这个一个本地缓存的设计，从外部看，`MyType::get` 是一个不可变方法（参数是 `&self`）获取缓存中的值，但内部需要更新 cache。这种"对外不可变，对内有副作用"的场景，编译器没法在 `&self` 上让你拿 `&mut HashMap`，只能用 `RefCell` 把检查推到运行时。

`RefCell<T>` 也有限制：只能单线程用。多线程要用 `Mutex<T>` 或 `RwLock<T>`。

### Rc<RefCell\<T>>：经典组合

把 `Rc` 和 `RefCell` 套起来，就得到了"多所有者 + 可变"的组合，这是 Rust 里非常常见的模式：

```rust
use std::rc::Rc;
use std::cell::RefCell;

let shared = Rc::new(RefCell::new(vec![1, 2, 3]));

let a = Rc::clone(&shared);
let b = Rc::clone(&shared);

a.borrow_mut().push(4);
b.borrow_mut().push(5);

println!("{:?}", shared.borrow());   // [1, 2, 3, 4, 5]
```

`Rc<RefCell<T>>` 在功能上接近 Java 里的"普通对象引用"，多个变量指向同一份数据，谁都能改。代价是：每次访问都要走一次 `borrow()` / `borrow_mut()`，运行时有少量开销，违反规则会 panic。

写图、树这种共享数据结构时，`Rc<RefCell<T>>` 几乎是逃不掉的。

### Arc\<T> 和 Mutex\<T>：多线程版本

`Rc` 和 `RefCell` 都只能在单线程用，因为它们内部的计数和借用跟踪都不是原子的。多线程版本是：

- `Arc<T>`（atomic reference counted）：多线程版本的 `Rc`
- `Mutex<T>` / `RwLock<T>`：多线程版本的 `RefCell`

`Arc` 跟 `Rc` 用法几乎一样，只是 clone 时的计数操作是原子的：

```rust
use std::sync::Arc;
use std::thread;

let data = Arc::new(vec![1, 2, 3]);

let mut handles = vec![];
for i in 0..3 {
    let data = Arc::clone(&data);
    handles.push(thread::spawn(move || {
        println!("thread {i} sees: {:?}", data);
    }));
}

for h in handles {
    h.join().unwrap();
}
```

`Mutex<T>` 就是 Java 的 `synchronized` + 一个对象的组合体，它把"锁"和"被保护的数据"绑在一起：

```rust
use std::sync::Mutex;

let m = Mutex::new(0); // m 不仅是一个锁，也是一个数据的载体
{
    let mut num = m.lock().unwrap();   // 拿锁
    *num += 1;
}   // num 离开作用域，锁自动释放
```

注意这里跟 Java 的本质差别：Java 的 `synchronized` 锁的是任意对象，编译器并不知道你具体要保护什么数据，靠程序员自觉；Rust 的 `Mutex<T>` 把数据装在锁里面，**没拿到锁就根本访问不到数据**。这是非常 Rust 的设计：把"必须先加锁"这件事直接做进类型系统里，让你不可能写错。

`Arc<Mutex<T>>` 是多线程下"多所有者 + 可变"的标准组合，对应单线程的 `Rc<RefCell<T>>`。并发这块后面有专门一节，到时候再展开。

新手刚开始写 Rust，最容易卡在所有权和借用上，往往会被建议"上 `Rc<RefCell<T>>` 解决一切"。这个建议对入门有用：能跑就比啥都强。但写多之后会发现，真正写得"地道"的 Rust 代码，会尽量避免这些智能指针，靠合理的所有权设计就能搞定。智能指针是工具，但用得多了说明设计上有冗余，能少用就少用。

## 十五、模块系统：怎么把代码组织起来

写到这里我们的代码都是单文件的小例子，真实项目肯定不能这么写。这一节我们看 Rust 怎么把代码组织成多个文件、怎么暴露 API、怎么管理可见性。

跟 Java 比，Rust 的模块系统有相似的地方，也有挺独特的地方。我们一边讲一边对比。

### 怎么对应 Java 多 module 项目

我们直接从一个真实场景切入。Java 项目里我们经常用 Maven/Gradle 把代码拆成多个 module，比如一个典型的 web 项目：

```
myapp/
├── pom.xml
├── myapp-api/      # 接口定义、DTO
├── myapp-data/     # 数据层，entity、repository
├── myapp-common/   # 公用工具
└── myapp-web/      # controller、REST endpoint，启动应用的入口
```

Rust 怎么对应？两个核心概念先记住：

- **crate**：Rust 的"编译单元"，一份独立编译出来的产物（库或可执行文件），对应 Java 的一个 Maven module / 一个 jar 包
- **module**（`mod` 关键字）：crate 内部的命名空间，对应 Java 的 package

这里要特别注意一下，Rust 的 module 跟 Java 的 module 名字一样但完全不是一回事。Java 的 module 对应 Rust 的 crate，而 Rust 的 module 对应 Java 的 package。

至于"多个 module 一起组成一个项目"这种形式，Rust 里叫 workspace。前面那个 Java 项目，Rust 写出来大概长这样：

```
myapp/
├── Cargo.toml          # workspace 配置
└── crates/
    ├── api/            # 类似 myapp-api
    │   ├── Cargo.toml
    │   └── src/lib.rs
    ├── data/           # 类似 myapp-data
    ├── common/         # 类似 myapp-common
    └── web/            # 类似 myapp-web，binary crate（src/main.rs），启动入口
```

每个子目录都是一个独立的 crate，有自己的 `Cargo.toml`。根目录的 `Cargo.toml` 用来声明 workspace：

```toml
# 根 Cargo.toml
[workspace]
resolver = "2"
members = ["crates/*"]
```

crate 之间通过路径依赖串起来：

```toml
# crates/web/Cargo.toml
[dependencies]
api = { path = "../api" }
data = { path = "../data" }
common = { path = "../common" }
```

跟 Maven 的 `<modules>` + `<dependency>` 思路完全对得上。整个 workspace 共享一个 `Cargo.lock` 和 `target/` 目录，编译出来的依赖可以复用，不会每个 crate 重复编译同一个东西。

也有把 crate 直接平铺在根目录、不另开 `crates/` 子目录的，两种风格都常见，看个人喜好。tokio、bevy 这种大型开源项目基本都是 workspace 组织。

至于 Cargo 文档里偶尔出现的"package"，一个 `Cargo.toml` 管的项目就是一个 package，里面默认包含一个 lib crate（`src/lib.rs`）加任意个 binary crate（`src/main.rs` 或 `src/bin/*.rs`）。简单项目里 package 跟 crate 基本是 1:1 的，平时不用刻意区分。

写个命令行小工具，`cargo new myapp` 单 crate 就够；要做多模块项目，再上 workspace。

### mod：定义模块

模块用 `mod` 关键字声明，最简单的形式是把模块写在同一个文件里：

```rust
mod network {
    pub fn connect() {
        println!("connecting...");
    }

    pub mod server {       // 模块可以嵌套
        pub fn run() {
            println!("server running");
        }
    }
}

fn main() {
    network::connect();
    network::server::run();
}
```

模块路径用 `::` 隔开，不像 Java 用 `.`。这跟 C++ 的命名空间风格一致。

### 文件组织：从单文件到多文件

把所有模块塞一个文件显然不现实。Rust 的文件组织规则其实很简单，但讲之前先强调一个跟 Java 非常不一样的地方，初学 Rust 几乎人人都会踩这个坑：

> **Rust 不会按文件系统自动把代码包含进项目，文件得在父模块里 `mod xxx;` 显式声明，编译器才认。**

Java 是按目录扫描的，`.java` 文件放对位置编译器就自动收。Rust 不是这样：你新建了 `src/foo.rs`，写了一堆代码，但只要没人在 `lib.rs` 或父模块里写 `mod foo;`，编译器就当这个文件不存在，`cargo build` 还照过。典型症状是新加的函数用起来报"找不到"，十有八九就是忘了在父模块里 `mod` 声明。

知道这点，我们再看具体规则。

假设我们写一个库 crate，入口是 `src/lib.rs`：

```rust
// src/lib.rs
mod network;   // 注意这里没有大括号，告诉编译器去找文件
mod utils;
```

这一行 `mod network;` 告诉编译器：去找一个叫 `network` 的模块，文件可能在：

- `src/network.rs`（推荐，新写法）
- `src/network/mod.rs`（老写法，仍然支持）

任选其一即可，但一个项目里一般统一用一种风格。

如果 network 模块本身又有子模块，按同样的规则继续。新写法下文件结构长这样：

```
src/
├── lib.rs              # mod network; mod utils;
├── network.rs          # 里面写: mod server; mod client;
├── network/
│   ├── server.rs       # network::server 的实现
│   └── client.rs       # network::client 的实现
└── utils.rs
```

注意 `network.rs` 和 `network/` 目录是配套的，`network.rs` 是模块本身，`network/` 目录里放它的子模块。这种"文件 + 同名目录"的组织看起来有点别扭，但用熟了就好。

老写法是用 `mod.rs`：

```
src/
├── lib.rs
├── network/
│   ├── mod.rs          # 这个文件就是 network 模块
│   ├── server.rs
│   └── client.rs
└── utils.rs
```

老项目里 `mod.rs` 还很常见，新项目大家更喜欢前一种风格。原因之一是：一堆 `mod.rs` 在 IDE 里打开后标签页全是 "mod.rs"，分不清楚。

### 路径：怎么引用东西

模块里的东西怎么引用？Rust 有几种路径写法：

```rust
// 绝对路径，从 crate 根开始
crate::network::server::run();

// 相对路径，从当前模块开始
network::server::run();

// self 表示当前模块
self::server::run();

// super 表示父模块，类似目录里的 ..
super::utils::helper();
```

`crate`、`self`、`super` 这三个关键字在路径里很常用，记住就行。

### pub：控制可见性

模块和模块里的东西默认都是**私有的**，这点跟 Java 不一样。Java 默认是 package-private，至少包内可见；Rust 默认连父模块都看不见。

要对外暴露，得加 `pub`：

```rust
mod network {
    fn private_helper() {}      // 只在 network 模块内可见

    pub fn connect() {           // 父模块可见
        private_helper();
    }
}
```

`pub` 还有几个变体，控制粒度更细：

```rust
pub               // 完全公开
pub(crate)        // 仅当前 crate 内可见，对 crate 外（比如别人用你的库时）不可见
pub(super)        // 仅父模块可见
pub(in path)      // 在指定路径内可见
```

`pub(crate)` 用得最多，意思是"我自己这个项目里随便用，但别人通过依赖引入这个 crate 的时候看不到"。

struct 的字段也是默认私有的，需要单独标 `pub`：

```rust
pub struct User {
    pub name: String,        // 字段公开
    age: u32,                // 字段私有
}
```

把 struct 标成 `pub` 不会让它的字段也跟着 `pub`，得逐个标。这点其实跟 Java 一致，`public class` 里的字段默认也不是 `public`，要单独写。

### use：把路径引入作用域

模块路径经常很长，每次写全名很烦。`use` 解决这个问题：

```rust
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("a", 1);
}
```

这个跟 Java 的 `import` 思路一样。也支持几个进阶用法：

```rust
// 重命名（Java 里不能给 import 起别名，得用全名）
use std::io::Result as IoResult;

// 同模块下多个东西
use std::collections::{HashMap, HashSet, BTreeMap};

// 引入模块本身和它的内容
use std::io::{self, Read};   // 引入 io 模块和 io::Read

// glob 引入（不推荐用在普通代码里，名字会污染）
use std::collections::*;
```

最后一个 `use std::collections::*;` 跟 Java 的 `import x.y.*;` 类似，但 Rust 社区里基本只在测试模块和 prelude 里用，普通代码里都老老实实把要用的东西列清楚。

### prelude：为什么很多东西不用 use

写到这里你应该会有个疑问：前面我们用 `Option`、`Result`、`String`、`Vec` 这些都没写过 `use`，为啥？

因为 Rust 有一个 prelude（预导入），里面放了一批最常用的类型和 trait，所有模块都自动 `use` 进来。这就是为啥 `Option`、`Vec` 你直接用就行。

你可以理解成 Java 里 `java.lang.*` 自动导入的机制，思路完全一样。不过 Rust 的 prelude 比 `java.lang` 范围大一些，里面除了类型还包含了一批 trait，比如 `Clone`、`Iterator`、`Drop` 等。

### pub use：重导出

`pub use` 是一个挺有用的进阶用法，意思是"把别的地方的东西重新暴露在我这里"：

```rust
// src/lib.rs
mod inner;

pub use inner::SomeType;   // 把 inner::SomeType 在 crate 根重新暴露
```

这样下游使用方就可以写 `mycrate::SomeType` 而不是 `mycrate::inner::SomeType`，对外提供更扁平的 API，同时保留内部的层级结构。一个常见的应用就是在 `lib.rs` 里集中重导出，给用户一个干净的 API 表面。

### workspace 的几个实用配置

回到一开始我们那个多 crate 项目。真实工程里，每个 crate 都重复写一遍 `edition = "2021"`、`serde = "1"` 这种依赖会很烦。Cargo 提供了几个 workspace 级别的配置帮你减少重复，写大项目几乎一定会用到。

第一个常用的是 **共享依赖版本**：

```toml
# 根 Cargo.toml
[workspace]
resolver = "2"
members = ["crates/*"]

[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
```

成员 crate 引用：

```toml
# crates/web/Cargo.toml
[dependencies]
serde.workspace = true        # 直接用 workspace 里定义的版本
tokio.workspace = true
api = { path = "../api" }
```

这样升级一次依赖版本，所有用到它的 crate 一起跟着升，不会出现"web 用 serde 1.0.150 而 data 用 1.0.180"的版本错位。Maven 的 `<dependencyManagement>` 思路类似，但 Cargo 这套写法明显比 pom.xml 干净。

第二个是 **共享 package 元数据**，比如 edition、version、license 这些：

```toml
[workspace.package]
edition = "2021"
version = "0.1.0"
license = "MIT"
authors = ["Your Name"]
```

成员里继承：

```toml
# crates/web/Cargo.toml
[package]
name = "web"
edition.workspace = true
version.workspace = true
license.workspace = true
```

第三个我个人觉得很重要的是 **统一的 lint 配置**。Rust 项目里大家通常会要求开很多 clippy 警告，可以集中管理：

```toml
# 根 Cargo.toml
[workspace.lints.clippy]
unwrap_used = "warn"
expect_used = "warn"
```

成员里：

```toml
# crates/web/Cargo.toml
[lints]
workspace = true
```

这样所有 crate 共享同一套规则，新加的 crate 也跟着一起被约束。

这几个配置加起来，整个 workspace 的依赖版本、编辑版本、lint 规则都统一了，新加一个 crate 时 `Cargo.toml` 可以写得非常薄，跟 Maven 父 pom 里集中管理 `<properties>` 的体验差不多，但写法更紧凑。

### 跟 Java 的几个差异点

讲到这里，模块系统的核心内容差不多了，最后回头梳理一下跟 Java 的几个关键差异：

- Java 默认 package-private，Rust 默认完全私有，需要主动 `pub`
- Java 的包 = 目录结构，强制对应；Rust 用 `mod` 声明，多一层灵活性
- Java 的 import 没法起别名，Rust 的 `use` 可以 `as`
- Java 没有 workspace 内置概念（Maven 多 module 是 Maven 的功能，不是 Java 的）；Rust 把 workspace 做进 Cargo 里
- Rust 的 prelude 让常用类型直接可用，跟 Java 的 `java.lang` 类似但范围更大

模块系统这块没啥难的，就是要熟悉一下规则。

## 十六、宏：Rust 的元编程

写到这里我们用过不少宏了：`println!`、`vec!`、`format!`、`#[derive(Debug)]` 等等。一直没专门讲宏是什么、它跟普通函数有啥区别。这一节我们把这个坑填上。

Java 程序员对"宏"这个词可能比较陌生，因为 Java 里没有真正意义上的宏系统，最接近的可能是注解处理器（annotation processor）和编译期的 Lombok，但能力跟 Rust 的宏比起来差挺多。我们一边讲一边对比。

说实话，宏代码真的很难写，这节主要是介绍一些基本的概念，以确保本文的完整性，实际工作中，肯定是把需求描述清楚，让 AI 来帮我们写的。别说写宏的代码了，就是看别人的实现都费劲，我们接触到一个宏，大概知道它是做了哪些事情，怎么用，我觉得就行了。

### 为啥要有宏？有哪几类宏？

最简单的疑问：函数能解决的事情为啥要搞宏？

答案是：函数解决不了某些事情。比如 `println!`：

```rust
println!("name: {name}, age: {age}");
```

这一行做的事是：在编译期检查字符串里的 `{name}` 和 `{age}` 是不是真的存在对应的变量、类型对不对。Java 的 `String.format` 是运行时才解析格式串，传错了运行时才报错；Rust 的 `println!` 编译期就把这事检查了。

再比如 `vec![1, 2, 3]`，它接受可变数量的参数。Rust 的普通函数没法接受可变参数（不像 Java 的 varargs），但宏可以。

总结一下，宏能做但函数做不了的事：

- 接受可变数量、可变形式的参数
- 在编译期生成代码（包括根据某些信息派生 trait 实现）
- 检查/解析字符串字面量、类型等编译期信息

Rust 里宏调用有下面几种形式：

```rust
println!(...)        // 函数式宏，带 ! 后缀
vec![...]            // 函数式宏，可以用 () 或 [] 或 {}
#[derive(Debug)]     // 派生宏，加在类型上
#[tokio::main]       // 属性宏，加在函数/类型上
```

Rust 的宏分两大类：

- **声明宏**（declarative macros）：用 `macro_rules!` 定义，通过模式匹配把宏调用展开成代码
- **过程宏**（procedural macros）：用 Rust 代码处理一棵语法树，再生成新的语法树

声明宏简单、易写，能力有限；过程宏更强大，但要单独搞一个 proc-macro crate，写起来麻烦。

### 声明宏：macro_rules!

声明宏其实就是一个正则替换游戏。

看一个简单例子：

```rust
macro_rules! say_hi {
    () => {
        println!("Hi!");
    };
}

fn main() {
    say_hi!();
}
```

这个宏没参数，就是把 `say_hi!()` 展开成 `println!("Hi!");`。语法是 `(模式) => { 展开 }`，可以有多个分支用 `;` 隔开。

带参数的版本：

```rust
macro_rules! greet {
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}

greet!("javadoop");   // 展开成 println!("Hello, {}!", "javadoop");
```

`$name:expr` 表示捕获一个表达式，命名为 name。`expr` 是片段类型（fragment specifier），还有 `ident`（标识符）、`ty`（类型）、`pat`（模式）、`stmt`（语句）等。

支持重复匹配，类似正则的 `*` 和 `+`：

```rust
macro_rules! my_vec {
    ( $( $x:expr ),* ) => {
        {
            let mut v = Vec::new();
            $(
                v.push($x);
            )*
            v
        }
    };
}

let v = my_vec![1, 2, 3];
```

`$( $x:expr ),*` 表示匹配零个或多个表达式、用逗号隔开。展开时用同样的 `$( ... )*` 包起来，里面引用 `$x` 就会按匹配的次数重复展开。

这个 `my_vec!` 其实就是标准库 `vec!` 宏的简化版。

声明宏的能力其实挺有限，主要就是模式匹配 + 重复，非常好理解。它适合写一些简单的语法糖、消除重复代码，但写不了太复杂的逻辑。

### 过程宏：三种形式

过程宏的能力强得多。它本质上是一段 Rust 代码，输入是 Token 流，输出也是 Token 流，中间你想干啥都行，通常会配合 `syn` 这个库去解析语法树，配合 `quote` 这个库去生成代码。

过程宏分三种形式。

**派生宏**（derive macros）：加在 struct/enum 上，自动生成 trait 实现。这个我们前面用过最多：

```rust
#[derive(Debug, Clone, PartialEq)]
struct User {
    name: String,
    age: u32,
}
```

`#[derive(Debug)]` 就是一个派生宏，它会展开成一个 `impl Debug for User` 的实现。整个标准库里 Debug、Clone、Default、PartialEq、Eq、Hash 这些都能 derive。第三方库也大量使用派生宏，比如 `serde` 的 `#[derive(Serialize, Deserialize)]`、`thiserror` 的 `#[derive(Error)]`。总之，它就是为了让 struct 实现某些 trait 来设计使用的。

**属性宏**（attribute macros）：可以加在几乎任何东西上，对它做转换：

```rust
#[tokio::main]
async fn main() { // 看上去好像这个方法不符合 Rust 的 main 入口规范，但是编译器在编译期会做展开的
    // ...
}

#[get("/hello")]
fn hello() -> String {
    String::from("hi")
}
```

`#[tokio::main]` 最终会把一个 `async fn main` 包装成一个普通的 `fn main`，里面建一个 tokio 运行时来跑，所以不要以为 Rust 支持其他形式的 main 函数，不过都是宏带给我们的假象。#[get("/hello")]` 是 web 框架（比如 actix-web、rocket）用来注册路由的。

**函数式宏**（function-like macros）：调用形式跟声明宏一样（带 `!`），但底层是过程宏：

```rust
let users = sqlx::query!("SELECT * FROM users WHERE id = ?", id);
```

`sqlx::query!` 是个函数式过程宏，它会在编译期连数据库验证 SQL 语法对不对、字段类型匹不匹配。这种"在编译期做你想象不到的事"是过程宏的拿手好戏。

### 跟 Java 注解处理器的对比

Java 的注解处理器（APT，annotation processing tool）是 Java 唯一接近"编译期生成代码"的机制。Lombok 的 `@Data`、MapStruct 的 mapper 生成、AutoValue 都是基于这个。

但跟 Rust 过程宏比，APT 有几个明显劣势：

- APT 只能加东西，不能改东西。你能让 Lombok 给 class 加 getter，但你没法让它修改已有方法。Rust 的属性宏可以完全替换掉一个函数。
- APT 是单独的处理阶段，IDE 支持差，常常需要插件配合（Lombok 在 IDE 里要装插件这事大家都很熟）。Rust 的过程宏跟编译器走同一条路，IDE 通过 rust-analyzer 直接看到展开后的代码，体验好得多。
- APT 没有过程宏里那种"函数式宏"调用形式。Java 里你只能 `@SomeAnnotation`，不能写 `someMacro!(...)` 这种调用。
- APT 的工具链相对简陋，写起来费劲。Rust 这边有 `syn` + `quote` + `proc-macro2` 这一套成熟工具链，社区生态也很活跃。

不过 Rust 过程宏也有代价，最大的就是**编译慢**，`syn` 解析整个 Rust 语法树是个相当重的活。一个项目里 `serde`、`tokio`、`sqlx` 这套用起来，编译时间会肉眼可见地变长。

### 啥时候用宏

讲完宏的能力，最后给点实用建议：

- 优先用函数。能用函数解决的问题就别用宏。
- 想消除一些样板代码、需要可变参数，用声明宏。
- 想给 struct 派生 trait，用派生宏。社区有现成的就用社区的，自己写一个派生宏的成本不低。
- 想做"看起来很魔法"的事，比如编译期验证 SQL、注册路由、定义 DSL，要写函数式过程宏或属性宏。

写宏（特别是过程宏）属于 Rust 比较高级的话题，本文不会教大家怎么写一个过程宏，我自己看宏的代码也是两眼一抹黑，因为宏的实现代码可读性都是很差的。碰到一个宏的时候，知道它们是什么、大概在做什么，这就够了。

到这里 Rust 语言层面比较独立的几块特性我们就讲得差不多了。下一节我们换个角度，看一个 Java 程序员特别关心的话题：异步编程。我们会从 Future 的设计讲起，看看 tokio 这个事实标准的 runtime 是怎么用的，以及在多线程 async 环境下最容易踩的几个坑。

## 十七、异步编程：async/await 和 tokio

终于到异步编程这一节了。说实话，我个人觉得这一节是 Rust 里"概念-实践"差距最大的一块，基础语法 `async` 和 `await` 看起来简单得不行，但真要写起项目来，能在编译器面前少挨几顿打就算赢。

这一节我们站在 Java 开发者的视角来理解 Rust 的异步。如果一上来就直接讲 Future、Pin、Waker，效果不一定好，所以我们先把 Java 这边的几张牌过一遍，再对着 tokio 是怎么做的来看，思路会比较顺。

### 先回顾一下 Java 这边的姿势

我们写 Java 服务端的时候，"异步"这件事大致经历了几个阶段，常用姿势串一下。

第一个是 Java Thread。new 一个 Thread 就是一个真正的内核线程，跟 OS 线程一对一。线程切换、栈空间、上下文保存这些事 OS 全包了，代价就是线程不便宜，一台机器上能开的 Thread 数量是有限的。所以我们一般不会自己 new Thread，而是丢到线程池里。

第二个是线程池，最熟的姿势：

```java
ExecutorService pool = Executors.newFixedThreadPool(8);
Future<String> f = pool.submit(() -> doSomething());
String result = f.get();
```

线程池的本质就两个东西：

- 一组固定数量的 worker 线程，长期 alive，循环从队列里取任务执行
- 一个任务队列，submit 方法把任务塞到队列里

这个模型记在脑子里，下面看 tokio 你会发现几乎是一回事。

第三个是 CompletableFuture，写一组有依赖的异步任务：

```java
CompletableFuture<String> fut = CompletableFuture.supplyAsync(() -> fetchUser(42))
    .thenApply(user -> user.toUpperCase())
    .thenAccept(System.out::println);
```

这里有一个非常关键的点：`supplyAsync` 一调用，任务就提交到线程池开始跑了。CompletableFuture 是 eager 的。这一点先记心里，因为 Rust 的 Future 是惰性的。

第四个是虚拟线程。JDK 21 之后，我们可以 `Thread.startVirtualThread(...)`，写出来还是同步代码长相，底层 N:M 调度，遇到阻塞 IO 自动让出。这是 Java 在异步领域的"答案"。如果你不了解虚拟线程，可以看我之前写的 《[Java 虚拟线程](/post/virtual-thread)》。

接下来，我们看看 Rust 世界里面异步是怎么玩的。

### tokio 的线程模型：把 Java 线程池里的 Thread 换成 Future

先讲一个跟 Java 不太一样的地方：Rust 标准库里没有异步 runtime。

Java 这边 `ForkJoinPool`、Loom 的调度器都是 JDK 自带，但 Rust 走的是"语言提供 async 语法和 Future trait，runtime 留给社区"的路线。原因也好理解，Rust 要覆盖从嵌入式到服务端的各种场景，硬塞一个 runtime 进标准库，要么嵌入式的人不爽（太重），要么服务器端的人不爽（太弱）。

实际项目里，社区基本统一用 tokio。它对应的角色你可以理解为"Rust 异步世界的 Netty + ForkJoinPool"。其他还有 async-std、smol，但 tokio 是事实标准，公司里九成以上的 Rust 项目都在用它。

加 tokio 依赖：

```toml
[dependencies]
tokio = { version = "1.52.3", features = ["full"] }
```

`features = ["full"]` 是开发期图省事的写法，实际项目你可能只开 `["rt-multi-thread", "macros", "net"]` 等具体特性，能瘦身不少。这块先不展开。

接下来看 tokio 默认的多线程 runtime 是什么样的：

- 启动 N 个 worker 线程（默认是 CPU 核数），每个 worker 是一个 OS 线程
- 每个 worker 自己持有一个本地任务队列
- 还有一个全局任务队列
- worker 之间会做 work-stealing：自己的队列空了，会从其他 worker 那"偷"任务来执行

是不是很眼熟？这个结构跟 Java 的 `ForkJoinPool` 几乎一模一样。

但有一个非常关键的区别：

- Java 的 `FixedThreadPool` 调度的是 Runnable/Callable，每个任务在 worker 上从头跑到尾，中间不让出
- tokio 调度的是 Future，每个任务可以执行到一半（碰到 await）让出，等条件好了再被唤醒接着跑

这就是 tokio 能做到"百万级并发"的关键：worker 还是只有那几个，但任务（Future）是非常轻量的对象，一个 worker 可以来回切换跑几千个 Future。这跟 Java 虚拟线程的思路是一致的，只是 Java 把这套切换机制做进了 JVM/JDK，Rust 把它做进了语言（async/await）加上用户态库（tokio）。

把这个记在心里，下面所有 tokio 的 API 都是围绕"怎么把 Future 喂给这个调度器"在转。

### Rust 的 Future：惰性的"任务对象"

铺垫完调度器，我们来看任务本身。Rust 的异步任务叫 Future，对应 Java 里的 `CompletableFuture<T>`。但有一个非常关键的差异：Rust 的 Future 是惰性的（lazy）。

什么意思？我们看代码：

```rust
async fn fetch_user(id: u64) -> String {
    println!("正在 fetch user {}", id);
    // 假装做了点事
    format!("user-{}", id)
}

#[tokio::main]
async fn main() {
    let fut = fetch_user(42);  // 注意，这一行其实啥也没干
    println!("Future 已创建，但还没开始执行");
    let user = fut.await;       // 到这里 Future 才真正开始跑
    println!("拿到 {}", user);
}
```

输出：

```
Future 已创建，但还没开始执行
正在 fetch user 42
拿到 user-42
```

看到了吧？调用 async fn 不会真的执行函数体，只是返回了一个 Future 对象，可以理解为打包了一个"待执行的任务"。直到你 `.await` 它（或者把它交给 runtime 去 poll），它才真正开始跑。

对比 Java 的 CompletableFuture：

```java
CompletableFuture<String> fut = fetchUser(42);  // 任务这一刻就提交到线程池了
fut.join();                                      // 这里只是在等结果
```

这是一个非常重要的心智差异，记心里：Rust 的 `async fn` 调用约等于"创建一个任务对象"，`.await` 才是"驱动它跑"。

那 Future 内部到底长啥样？我们不打算扒到 Pin、Waker 那么深，了解一个事实就够了：编译器会把 async 块编译成一个状态机 struct，每次被 poll 推进一步，碰到 await 就把状态保存好返回，等条件满足再被叫起来继续跑。这个事实后面讲 Send 的时候有用。

### #[tokio::main]：把 main 函数变成异步入口

Rust 的 main 函数必须是同步的，但我们想在里面写 async 代码怎么办？tokio 提供了一个属性宏：

```rust
#[tokio::main]
async fn main() {
    println!("hello async world");
}
```

这个宏背后展开成什么呢？大概是这样：

```rust
fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        println!("hello async world");
    });
}
```

也就是说，它给你创建了一个 tokio runtime（前面说的那个 worker 线程池就是这时候起来的），然后用 `block_on` 阻塞地等你的 async 块跑完。`block_on` 这个方法你也可以手动用，比如在测试或者跟同步代码混用的场景。

### spawn / join! / select!：往调度器里喂 Future

接下来看几个最常用的 API。我们对着 Java 的 `executor.submit` / `allOf` / `anyOf` 看，会发现一一对应。

#### tokio::spawn：对应 Java 的 executor.submit

把一个 Future 交给 runtime 去调度，立刻拿到一个 JoinHandle。这个 handle 跟 Java 的 Future 概念上是一回事，await 它能拿到任务的结果：

```rust
use tokio::time::{sleep, Duration};

async fn task(name: &str, secs: u64) {
    println!("{} 开始", name);
    sleep(Duration::from_secs(secs)).await;
    println!("{} 结束", name);
}

#[tokio::main]
async fn main() {
    let h1 = tokio::spawn(task("A", 1));  // 立刻提交到 runtime
    let h2 = tokio::spawn(task("B", 2));

    h1.await.unwrap();
    h2.await.unwrap();
}
```

spawn 出去的任务有可能跑在调度器的任意一个 worker 线程上，所以它要求 Future 是 `Send` 的。Send 是啥下一节专门讲。

#### tokio::join!：等若干 Future 全部完成

类似 `CompletableFuture.allOf(...).join()`，但有个区别：join! 不会把 Future 提交到调度器，而是在当前任务里并发推进它们：

```rust
#[tokio::main]
async fn main() {
    // 串行版本：A 跑完再跑 B，总共 3 秒
    task("A", 1).await;
    task("B", 2).await;

    // 并发版本：A B 同时跑，总共 2 秒
    tokio::join!(task("A", 1), task("B", 2));
}
```

注意"并发地"不一定是"并行地"。join! 的几个 Future 都在同一个任务里，谁 await 阻塞了就让别的接着跑，整体还是单线程的逻辑。所以 join! 不要求 Send。

什么时候用 spawn、什么时候用 join!？

- 想"独立调度、可能跨线程并行执行"用 `spawn`
- 想"在当前任务里并发推进、不要求 Send"用 `join!`

#### tokio::select!：等多个 Future 中先到的那个

类似 `CompletableFuture.anyOf(...)`：

```rust
#[tokio::main]
async fn main() {
    tokio::select! {
        _ = sleep(Duration::from_secs(1)) => {
            println!("超时了");
        }
        result = fetch_user(42) => {
            println!("拿到 {}", result);
        }
    }
}
```

这个非常常用，最典型的几个场景是给 IO 加超时、监听 ctrl-c 退出信号、从多个 channel 里抢消息等等。

select! 有个坑：没有被选中的那个 Future 会被直接丢弃。所以如果你的 Future 不是"取消安全"的（cancel-safe），可能丢了之后状态就乱了。一般标准库和 tokio 自己的 API 都会标注哪些是 cancel-safe，这里先不展开，碰到了再说。

### Send 和 Sync：把"线程安全"做进类型系统

下面这一节单独拎出来讲，因为它在 Rust 异步里是绕不过的一块，但是它属于比较复杂的话题了，对于初学者来说，大概知道怎么回事就行了。理解了它，前面 spawn 为啥要 Send，还有后面碰到的各种 "xxx is not Send/Sync" 报错，会清晰很多。

我们先看一下 Java 这边的处境。写 Java 的时候，"这个类型能不能跨线程用"这件事是没有写进类型系统的。`ArrayList` 不是线程安全的、`ConcurrentHashMap` 是线程安全的，但这件事编译器根本不知道，你把 ArrayList 共享到多个线程里去用，编译能过、运行也不会立刻报错，只是数据可能就乱了。我们靠的是 Javadoc、靠经验、靠出过事的项目才知道哪个能用哪个不能用。

Rust 的思路完全不一样：把"能不能跨线程"做成两个 marker trait，让编译器替我们检查。这就是 Send 和 Sync。先记一句话总结：

- Send：一个值能不能"移动到另一个线程"，重点是所有权转移
- Sync：一个值能不能"被多个线程共享引用"，重点是共享引用

#### Send：能否把所有权"移动"到另一个线程

如果一个类型 T 是 Send，意思是把 T 的值的所有权从一个线程交给另一个线程是安全的。绝大部分类型都是 Send：`i32`、`String`、`Vec<T>`（要求 T: Send）、自己写的普通 struct 这些默认都是。

例外有几个：

- `Rc<T>`：引用计数没用原子操作，跨线程并发改计数会竞争，所以不是 Send
- `MutexGuard<'_, T>`：std 的 Mutex 是 OS 线程级的锁，guard 必须在加锁那个线程释放，所以不是 Send
- 裸指针 `*const T` / `*mut T`：编译器对它一无所知，不敢替你做保证

它们都有对应的"可跨线程版本"：`Arc<T>` 用原子计数，是 Send；tokio 自带的 `tokio::sync::MutexGuard` 也是 Send 的。

#### Sync：能否被多个线程"共享引用"

定义稍微绕一点：如果 `&T` 是 Send，那么 T 就是 Sync。换句话说，T 是 Sync 意味着多个线程同时持有 `&T` 是安全的。举几个例子：

- `i32`、`String`、`Vec<T>`（要求 T: Sync）这些都是 Sync
- `Mutex<T>`（要求 T: Send）是 Sync，这正是它存在的意义，让 T 能被多个线程共享 + 修改
- `RefCell<T>` 不是 Sync。它的借用检查是单线程的，多线程共享 `&RefCell` 然后并发借用，根本不安全
- `Cell<T>` 也不是 Sync，原因类似

我们之前在智能指针那一节就总结过一个经验法则，现在把它跟 Send/Sync 对上号了：

- 单线程下要"共享 + 内部可变"：`Rc<RefCell<T>>`
- 多线程下要"共享 + 内部可变"：`Arc<Mutex<T>>` 或 `Arc<RwLock<T>>`

#### 自动派生

Send 和 Sync 都是 auto trait，编译器会根据字段自动派生。规则非常简单：所有字段都 Send，自己才 Send；只要有一个字段不是 Send，自己就不是。Sync 同理。

所以绝大部分时候你不用主动想这件事，只有当 struct 里夹了 Rc、RefCell、裸指针这种"特殊成员"时，类型才会莫名其妙不是 Send/Sync，顺着字段找原因就行。

极少数情况下你需要手动给类型打上 Send/Sync 标记（比如自己用裸指针实现了一个能保证并发安全的数据结构），这时候要写 `unsafe impl Send for MyType {}`。这个 `unsafe` 是在说：编译器没法替你检查了，出了问题你自己负责。新手碰到的概率很低，留个印象就行。

### 把 Send 套回 spawn：std Mutex 的经典坑

铺垫完 Send 和 Sync，我们再回头看 `tokio::spawn` 的签名（简化版）：

```rust
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
    F: Future + Send + 'static,
    F::Output: Send + 'static,
```

为啥要求 Future 是 Send，前面已经说过，多线程 runtime 下任务可能在不同 worker 线程之间移动。真正的问题是：什么样的 async 块会被编译成 Send 的 Future？

前面我们讲 Future 内部时提过，编译器会把 async 块编译成一个状态机 struct，所有跨 await 还活着的局部变量都会成为这个 struct 的字段。再叠加 Send 的派生规则（所有字段都 Send，整个 struct 才 Send），判断标准就出来了：**跨 await 持有的变量是不是全都是 Send**。

最经典的反例就是 std Mutex 的 guard：

```rust
use std::sync::Mutex;

#[tokio::main]
async fn main() {
    let m = Mutex::new(0);

    tokio::spawn(async move {           // 编译错误就报在这一行：future is not `Send`
        let mut guard = m.lock().unwrap();
        *guard += 1;
        do_something_async().await;     // guard 跨过了这个 .await，没机会被 drop
        // guard 一直活到这里才 drop
    });
}
```

编译器会告诉你 MutexGuard 不是 Send，所以这个 async 块也不是 Send，所以不能 spawn。两个解法：

办法一，缩短 guard 的存活范围，不要让它跨 await：

```rust
{
    let mut guard = m.lock().unwrap();
    *guard += 1;
}  // guard 在这里就 drop 了
do_something_async().await;
```

办法二，用 `tokio::sync::Mutex`，这是个异步锁，它的 guard 是 Send 的：

```rust
use tokio::sync::Mutex;

let m = Mutex::new(0);
let mut guard = m.lock().await;  // 注意是 .await，不是 .unwrap()
*guard += 1;
do_something_async().await;  // OK
```

实践经验：能用办法一就用办法一。因为 tokio 的 Mutex 比 std 的贵，能不用就别用。

### 顺便说一下：为什么有了 Send 还要 Sync？

讲到这里可能有读者会冒出一个疑问：Send 已经管了"跨线程"这件事，为啥还要再搞一个 Sync？两个 trait 听起来挺像，合并成一个不行吗？

关键就在 `&T` 上。

对绝大多数类型，`&T` 确实就是"只读"。这种情况下"独占 T"和"共享 `&T`"在安全性上没区别，Send 一个 trait 就够了。但 Rust 里有一类很特别的"内部可变"类型，最典型的就是 `Cell<T>`：

```rust
use std::cell::Cell;

let c: Cell<i32> = Cell::new(0);
let r: &Cell<i32> = &c;
r.set(42);  // 拿着 &Cell 就能改值
```

`&Cell` 看起来是不可变引用，但实际上能通过它修改内部的值，这就是所谓的"内部可变性"。这下 Send 和 Sync 就分了家：

- `Cell<T>` 是 Send 吗？是的。整个 Cell 搬到另一个线程是安全的，因为同一时刻只有一个线程拥有它
- `Cell<T>` 是 Sync 吗？不是。两个线程同时拿着 `&Cell<i32>`、都调 `.set(...)`，Cell 内部没做任何同步，data race 直接发生

Send 通过、Sync 不通过，这就是 Sync 必须独立存在的意义。如果 Rust 里所有 `&T` 都意味着"只读"，Sync 确实就是冗余的；偏偏因为有 Cell、RefCell、UnsafeCell 这种"`&T` 也能改"的设计，"共享引用是否安全"必须单独检查一次。

反过来看 `Mutex<T>` 你也会更清楚 Sync 的价值。`Mutex<T>` 是 Sync 的，因为内部本来就有锁，多个线程并发拿着 `&Mutex<T>` 调 `.lock()` 是安全的，锁会序列化它们。Mutex 存在的意义就是把一个"非 Sync"的 T 包起来，让它可以被并发共享。

### 阻塞操作：不要污染 runtime

这是另一个非常容易踩的坑，Java 那边其实也有完全对应的问题。看这个代码:

```rust
async fn bad() {
    std::thread::sleep(Duration::from_secs(5));  // 错！
    // 同步的文件读、阻塞的 HTTP 库、CPU 密集的计算，都不该这么用
}
```

为啥不行？回到我们最开始说的那个调度器结构：tokio 的 worker 线程数量是有限的（默认是 CPU 核数）。你这 5 秒的同步 sleep 把一个 worker 占着不动，整个 runtime 上跑的其他任务都受影响。这跟在 Netty 的 EventLoop 上写阻塞代码是一个性质的错误。如果你之前看过我写的 Java 虚拟线程那篇文章，应该一下子就 get 到了。

正确的做法是：

- 有异步版本就用异步版本：`tokio::time::sleep`、`tokio::fs`、`reqwest` 等
- 实在要跑同步代码：用 `tokio::task::spawn_blocking`

```rust
let result = tokio::task::spawn_blocking(|| {
    // 这里跑同步代码，会被丢到一个专门的"阻塞线程池"
    expensive_sync_computation()
}).await.unwrap();
```

`spawn_blocking` 维护一个独立的线程池（默认 512 个线程），专门给你跑那些"不得不阻塞"的代码。它跟 worker 线程池是分开的，所以不会互相影响。Java 这边类似的姿势是给阻塞任务单独配一个线程池，思路一致。

### Channel：异步任务之间的通信

跟 Java 里 `BlockingQueue` 类似，tokio 提供了几种 channel。最常用的是 `mpsc`（多生产者单消费者）：

```rust
use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);  // 缓冲区大小 32

    // 生产者
    tokio::spawn(async move {
        for i in 0..5 {
            tx.send(i).await.unwrap();
        }
        // tx 在这里 drop，rx.recv() 之后会收到 None
    });

    // 消费者
    while let Some(value) = rx.recv().await {
        println!("收到 {}", value);
    }
}
```

发送和接收都是 async 的，缓冲区满了会让出，缓冲区空了也会让出。

tokio 还有 `oneshot`（一次性传一个值，常用于"任务完成的通知"）、`broadcast`（多消费者，每个消费者都能收到全部消息）、`watch`（适合配置热更新这种场景），按需选用。

### 异步小结

异步这块涉及的东西不少，我们用几个要点串一下：

- tokio 多线程 runtime 跟 Java 的 `ForkJoinPool` 结构很像：N 个 worker 线程 + 本地队列 + work-stealing。区别在于 tokio 调度的是 Future（很轻量、可让出），Java 线程池调度的是从头跑到尾的 Runnable
- Rust 的 Future 是惰性的，调用 async fn 不等于开始执行，必须 `.await` 或者交给 runtime 去 poll；而 Java 的 CompletableFuture 是 eager 的
- Rust 标准库不带 runtime，事实标准是 tokio，入口一般用 `#[tokio::main]`
- 想"独立调度、可能跨线程并行"用 `tokio::spawn`（对应 `executor.submit`）；想"并发等多个" 用 `tokio::join!`（类似 `allOf`）；想"等先到的"用 `tokio::select!`（类似 `anyOf`）
- Send 和 Sync 这两个 marker trait 把"线程安全"做进了类型系统，弥补了 Java 这边只能靠 Javadoc 的不足。Send 表示"可跨线程转移所有权"，Sync 表示"可跨线程共享引用"
- 多线程 runtime 下 spawn 要求 Future 是 Send，最常见的报错就是 std Mutex 的 guard 跨 await
- 不要在 async 里调用阻塞 API，类比 Netty EventLoop 上写阻塞代码；非要做同步重活，用 `spawn_blocking`
- 任务间通信用 tokio 的 channel，最常用的是 `mpsc`，按场景还有 `oneshot`、`broadcast`、`watch`

## 十八、错误处理实践：anyhow 和 thiserror

前面 `Result` 那一节我们已经知道了 Rust 错误处理的基本面：用 `Result<T, E>` 表达一个可能失败的操作，用 `?` 操作符做早返回。

但真要写一个像样的项目，光靠标准库提供的这些其实是不够的。我们看一个非常常见的小例子：一个函数里要先读文件，再把文件里的字符串 parse 成数字。

这两步会失败的方式是不一样的：

- 读文件失败：`std::io::Error`
- parse 失败：`std::num::ParseIntError`

```rust
fn read_number(path: &str) -> Result<i32, ???> {
    let content = std::fs::read_to_string(path)?;  // 这里会抛 std::io::Error
    let n: i32 = content.trim().parse()?;          // 这里会抛 ParseIntError
    Ok(n)
}
```

`???` 这一格写啥？两个错误类型完全不一样，但函数的返回类型只能写一个。

这就是社区里 anyhow 和 thiserror 这两个库要解决的问题。简单说，思路有两种：

- 应用代码（写 main、写业务逻辑）：用 anyhow，把所有错误统一包成一个万能错误类型，怎么爽怎么来。
- 库代码（要发布出去给别人用的）：用 thiserror，定义清晰、结构化的错误枚举，让使用方能区分不同的失败原因。

类比 Java 的话，anyhow 就像直接 throw 一个 `RuntimeException`，把原因塞进 message 和 cause；thiserror 就像写一组语义清晰的 checked exception，让调用方能 catch 到具体的子类。

我们分别看看。

### thiserror：定义结构化的错误类型

thiserror 是一个过程宏库，用 derive 自动给你生成 `std::error::Error` 这个 trait 的实现。

回到上面那个例子：

```rust
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ReadNumberError {
    // 文件读不出来
    #[error("读文件失败: {0}")]
    Io(#[from] std::io::Error),

    // 解析数字失败
    #[error("解析数字失败: {0}")]
    Parse(#[from] std::num::ParseIntError),
}

pub fn read_number(path: &str) -> Result<i32, ReadNumberError> {
    let content = std::fs::read_to_string(path)?;  // io::Error 自动转成 ReadNumberError::Io
    let n: i32 = content.trim().parse()?;          // ParseIntError 自动转成 ReadNumberError::Parse
    Ok(n)
}
```

我们看几个关键点：

1. `#[derive(Error, Debug)]`：thiserror 帮我们实现 `std::error::Error`，`Debug` 是标准库自带的。
2. `#[error("...")]`：定义错误的 `Display` 实现，也就是错误信息怎么打印。
3. `#[from]`：这个是最关键的。它会生成一个 `From<std::io::Error> for ReadNumberError` 的实现。有了 `From`，`?` 操作符就会自动帮我们做转换。

调用方拿到 `ReadNumberError` 之后，可以 match 它的具体变体来做不同的处理：

```rust
match read_number("config.txt") {
    Ok(n) => println!("拿到 {}", n),
    Err(ReadNumberError::Io(_)) => eprintln!("文件没找到，用默认值"),
    Err(ReadNumberError::Parse(_)) => eprintln!("文件内容格式不对"),
}
```

这就是结构化的好处，错误的类型本身就承载了语义。库的作者把错误枚举设计好，使用方就能优雅地处理不同的失败场景。

类比 Java，thiserror 大致相当于：

```java
public class ReadNumberException extends Exception {
    public static class Io extends ReadNumberException { ... }
    public static class Parse extends ReadNumberException { ... }
}
```

只不过 Rust 这边用的是 enum，而且 thiserror 把模板代码（错误信息、cause 链、From 转换）全给你生成了，省心很多。

### anyhow：应用代码的省心包

anyhow 走的是另一条路。它对外只暴露一个类型：`anyhow::Error`，可以包住任何实现了 `std::error::Error + Send + Sync + 'static` 的错误。

我们再写一遍那个例子：

```rust
use anyhow::Result;  // 等价于 Result<T, anyhow::Error>

fn read_number(path: &str) -> Result<i32> {
    let content = std::fs::read_to_string(path)?;
    let n: i32 = content.trim().parse()?;
    Ok(n)
}
```

代码瞬间清爽。注意几个点：

1. `anyhow::Result<T>` 是 `Result<T, anyhow::Error>` 的别名。
2. 任何实现了标准 `Error` trait 的错误，`?` 都能自动转成 `anyhow::Error`，我们不用自己写 `From`。
3. main 函数也可以用：

```rust
fn main() -> anyhow::Result<()> {
    let n = read_number("config.txt")?;
    println!("拿到 {}", n);
    Ok(())
}
```

anyhow 还有一个特别实用的功能，叫 `.context()`，可以给错误链上加一层信息：

```rust
use anyhow::{Context, Result};

fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("读配置文件失败: {}", path))?;
    let config: Config = serde_json::from_str(&content)
        .with_context(|| "解析 JSON 失败")?;
    Ok(config)
}
```

最终如果失败，打印出来的错误是这样的：

```
Error: 解析 JSON 失败

Caused by:
    0: 读配置文件失败: /etc/myapp/config.json
    1: No such file or directory (os error 2)
```

这个 context 链就像 Java 里的 `Exception.getCause()` 一样，把错误传递的路径完整保留下来，debug 的时候非常好用。

### 啥时候用哪个

这个问题社区里讨论了很多次，常用的标准是：

- 写应用、写工具、写 main：用 anyhow。一个类型搞定一切，代码简洁，错误信息友好。反正最终错误就是给人看的（打日志、退出码、用户提示），结构化没意义。
- 写库（要发布给别人用的 crate）：用 thiserror。库的使用者需要根据错误类型做不同的逻辑分支，你要是给人扔一个 `anyhow::Error`，别人没法 match，只能拿到一段字符串。

混用其实也很常见，也是我个人比较推荐的做法：库内部用 thiserror 暴露规整的错误枚举，应用代码用 anyhow 把这些错误聚合在一起处理。

最后再说几条实践经验：

- 库代码里不要 panic（除非真的是 bug 兜底），把错误一律用 Result 暴露给调用方
- 应用的 main 直接写成 `fn main() -> anyhow::Result<()>`，让 anyhow 帮我们打印错误链，省掉自己写 `eprintln!`
- 调试不顺利的时候，加 `RUST_BACKTRACE=1` 跑一下，anyhow 会顺手把 backtrace 一起打出来

到这里我们基本就把日常错误处理摸熟了。最后一节我们看看 Rust 生态里几个绕不开的库。

## 十九、常用 crate 生态一览

写 Java 项目，光会语法是不够的，还得知道 Jackson、Guava、SLF4J、Spring 这些标配。Rust 这边也是一样，社区里有几个事实上的必装件。

这一节就是清单式地过一下这些 crate，每个简单介绍 + 跟 Java 对比，让你后面真要动手写项目的时候心里有谱。具体的细节我都不展开，每个 crate 的官方文档都写得很好，到时候去翻就行。

### serde：对应 Jackson

序列化反序列化的事实标准，地位相当于 Java 的 Jackson。

serde 本身只是个框架，定义了 `Serialize` 和 `Deserialize` 这两个 trait，至于具体跟哪种格式打交道（JSON、YAML、TOML、bincode、MessagePack），由配套的 crate 决定。最常用的是 serde_json。

```toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```

```rust
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    id: u64,
    name: String,
    #[serde(default)]      // 反序列化时，字段缺失就用 Default::default()
    active: bool,
}

fn main() -> anyhow::Result<()> {
    let json = r#"{"id": 1, "name": "alice"}"#;
    let user: User = serde_json::from_str(json)?;
    println!("{:?}", user);  // User { id: 1, name: "alice", active: false }

    let s = serde_json::to_string(&user)?;
    println!("{}", s);
    Ok(())
}
```

跟 Java Jackson 对比着看：

- `#[derive(Serialize, Deserialize)]` 对应 Jackson 的注解（`@JsonProperty` 之类的）。
- `#[serde(rename = "user_id")]`、`#[serde(default)]`、`#[serde(skip)]` 这些跟 Jackson 注解一一对应。
- serde 在编译期生成代码，Jackson 主要靠反射，所以 serde 的性能更好，代价是报错信息会稍微绕一些。

我个人感觉 serde 是 Rust 里最让我惊艳的库之一，它把"用 derive 生成代码"这件事玩到了极致。

### reqwest：对应 OkHttp / HttpClient

HTTP 客户端的事实标准。底层基于 hyper，对外提供同步和异步两套 API。

```toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
```

```rust
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let resp: serde_json::Value = reqwest::get("https://httpbin.org/get")
        .await?
        .json()
        .await?;
    println!("{:#?}", resp);
    Ok(())
}
```

对比 Java：

- 同步用法跟 OkHttp 思路一样：构造请求、send、拿 response。
- 异步用法跟 Java 11 引入的 `HttpClient` 异步 API 类似，但 Rust 这边是真正的 async/await，没有 `CompletableFuture` 链式调用那一坨。
- 配合 serde，`.json::<T>()` 直接把 body 反序列化成 struct，比 Java 那边写 `objectMapper.readValue(body, User.class)` 还顺。

顺便提两个相关的搭档：想做 HTTP server，用 axum 或 actix-web（对应 Spring Web / Vert.x）；想做 gRPC，用 tonic（对应 grpc-java）。

### tracing：对应 SLF4J + Logback

日志和 telemetry 的事实标准。注意 tracing 不只是日志库，它的设计目标是结构化的可观测性，包含了 log、span（链路）、event。简单理解，tracing 一个就替代了 SLF4J + Logback + 部分 OpenTelemetry 的角色。

跟 SLF4J 一样，tracing 本身只是一套 facade，真正把日志写出去的是 subscriber：

```toml
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
```

```rust
use tracing::{info, warn, instrument};

#[instrument]   // 给函数自动加 span，进入退出都会有日志，参数也会自动打出来
fn handle_request(user_id: u64) {
    info!(user_id, "开始处理请求");

    if user_id == 0 {
        warn!("user_id 为 0，可能是异常请求");
    }

    info!("处理完成");
}

fn main() {
    tracing_subscriber::fmt::init();   // 最简单的 subscriber，把日志打到 stderr
    handle_request(42);
}
```

输出大致是：

```
2026-05-07T08:00:00Z  INFO handle_request{user_id=42}: myapp: 开始处理请求 user_id=42
2026-05-07T08:00:00Z  INFO handle_request{user_id=42}: myapp: 处理完成
```

注意输出里的 `handle_request{user_id=42}` 这一段，这是 tracing 自动带上的 span 信息，整个请求的调用链一眼就能串起来。

类比 Java：

- `info!` / `warn!` / `error!` 这套宏，对应 SLF4J 的 `log.info(...)`。
- `#[instrument]` 对应 Spring AOP + 切面打日志的那种效果。
- subscriber 的替换，对应 Logback 配置 appender。
- 结合 `tracing-opentelemetry`，可以把 span 直接打到 Jaeger / Tempo / Datadog，对应 Java 那边的 micrometer-tracing。

如果你只是想要简单的"打日志"，社区还有一个更轻量的选择叫 `log` + `env_logger`，跟 SLF4J 的简单使用类似。但我建议直接上 tracing，未来有 telemetry 需求的时候可以平滑接上。

### sqlx / sea-orm：对应 MyBatis 和 JPA

数据库这块 Rust 没有像 Java 那样大一统的方案。社区里目前的两个主流：

- sqlx：偏 SQL-first，跟 Java 的 MyBatis、JdbcTemplate 类似。你自己写 SQL，sqlx 提供异步执行、参数绑定、结果映射。最大的特色是编译期检查 SQL，用 `sqlx::query!` 宏写的 SQL，会在编译时连到一个真实的数据库做语法和字段校验。
- sea-orm：偏 ORM，跟 Java 的 JPA / Hibernate 类似。提供实体定义、查询构建器、关系映射，让你少写 SQL。

我们各看一段示例。

sqlx 风格（自己写 SQL）：

```rust
use sqlx::PgPool;

#[derive(sqlx::FromRow)]
struct User {
    id: i64,
    name: String,
}

async fn find_user(pool: &PgPool, id: i64) -> anyhow::Result<Option<User>> {
    let user = sqlx::query_as::<_, User>("SELECT id, name FROM users WHERE id = $1")
        .bind(id)
        .fetch_optional(pool)
        .await?;
    Ok(user)
}
```

写起来其实跟 Spring 里用 Mybatis 差不多，SQL 自己写，结果映射到 struct。

sea-orm 风格（ORM）：

```rust
use sea_orm::*;

let user: Option<user::Model> = User::find_by_id(1)
    .one(&db)
    .await?;
```

跟 JPA 的 `userRepository.findById(1)` 几乎一对一对应。

怎么选？我个人的经验是：

- 项目以业务逻辑为主、SQL 不复杂的，sea-orm 上手快、心智负担小。
- 性能敏感、SQL 比较定制化、或者团队 SQL 功底好的，sqlx 更合适，编译期校验 SQL 是个非常香的特性。
- 真不知道选哪个，从 sqlx 开始。它更轻量，万一以后想换 ORM 也好换。

### 其他值得知道的几个

按使用频率从高到低简单列一下，篇幅有限就不展开了：

- axum：最主流的 HTTP server 框架，对应 Spring Web。
- tokio-util / futures：异步工具集，比 tokio 自带的更全。
- rayon：数据并行（多核 CPU 跑并行迭代器），对应 Java 的 parallel stream，但更扎实。
- chrono / time：日期时间库，对应 Java 8+ 的 java.time。
- uuid：生成 UUID。
- regex：正则。
- once_cell / lazy_static：全局单例和懒加载，新版本里 `std::sync::OnceLock` 已经原生支持。
- criterion：性能基准测试，对应 Java 的 JMH。

碰到一个新需求，习惯性去 [crates.io](https://crates.io) 搜一下，基本都能找到对应的库。社区生态这几年发展非常快，体验已经接近 Java、Python 这些老牌生态了。

## 总结

到这里，写一个像样的 Rust 项目所需要的基本面我们就过完了：语言、异步、错误处理、生态。剩下的就是去写代码、去翻文档、去跟编译器斗智斗勇。

最后一句老生常谈：Rust 学起来确实苦，学习曲线异常陡峭，但它逼着你把很多 Java 里"先跑起来再说"的问题想清楚，长远看对写代码的功力是有帮助的。希望本文能给你一个相对平滑的起手姿势，剩下的就交给时间了。

（全文完）

