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

本文就是一时兴起，想写一篇给 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 麻烦就麻烦在这里，它几乎把这些全部改掉了：

- 对象默认放栈上，要在堆上分配得显式用 `Box`、`Vec`、`String` 这类。
- 没有 GC，靠所有权 + 借用检查在编译期把内存安全管好。
- 参数传递可能会把值“拿走”，原变量就不能用了。
- 多线程共享和可变性都不是想 share 就 share，类型系统（`Send`/`Sync`）会拦你。
- 没有 null，没有 checked exception。空值用 `Option<T>`，错误用 `Result<T, E>`。

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 = "2021"

[dependencies]
# 在 java 里我们叫 jar 包，在 rust 里叫 crate
# 版本格式是强制要求，不像 java 那么自由，因为它涉及到版本自动升级
rand = "0.8.5"
```

接着是构建命令：

```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 文件来固化。

应用项目一般要把 `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 我们前面提过一嘴，它就是 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。

图大概这样：

```text
move 之前：

s1 ───> String { ptr, len, capacity } ───> 堆内存 "hello"

move 之后：

s1  失效

s2 ───> String { ptr, len, capacity } ───> 堆内存 "hello"
```

注意，这里堆上的 `"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);         // 等价写法
```

`&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 也挺啰嗦的，所以 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())
}
```

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

`?` 也可以作用在 `Option` 上，含义是"是 None 就直接 return None"，思路一致。

## 八、集合：Vec、HashMap

// todo

我会慢慢补充完整，大家也可以提提意见。
