Rust 学习笔记

一份覆盖完整知识点的 Rust 学习笔记。重点讲"为什么这么设计"(费曼式 📌 讲解),配代码示例。

怎么用这份笔记

  1. 学习/复习 → 顺序看正文,重点看 📌 类比和"为什么"
  2. 查语法 → 翻对应章节,代码示例可直接复制运行
  3. 深入 → 理解所有权、生命周期、并发安全三大核心概念

图表在 /Rust学习笔记/diagrams/ 目录。图注名即原图文件名,看图注就能直接找到原图用 draw.io 编辑。


1 · Rust 概述与设计理念

1.1 Rust 是什么

  • Rust 是一门系统级编程语言,由 Mozilla 的 Graydon Hoare 于 2006 年开始开发,2010 年首次公开,2015 年发布 1.0
  • 核心目标:零成本抽象的内存安全 + 线程安全,同时保持 C/C++ 级别的性能
  • 没有 GC:不靠垃圾回收管理内存,靠所有权 (Ownership) 系统在编译期保证内存安全

图 1 \xb7 Rust 内存安全设计

TIP

📌 打个比方:C/C++ 像给你一把没保险的枪——威力大但容易走火。Java/Go 像配了一个保镖(GC)——安全但有开销。Rust 像一把带保险的枪——编译器在编译时检查你每次扣扳机是否安全,通过就运行时不会走火。保险本身不消耗子弹(零成本)。

1.2 核心设计权衡

问题 C/C++ Java/Go Rust
内存安全 手动管理(易出错) GC 自动回收 编译期所有权检查
线程安全 自行保证 运行时检查 编译期 Send/Sync
性能 极快 GC 带来开销 零成本抽象,与 C/C++ 持平
学习曲线 中等 低 高
TIP

📌 为什么没有 GC 也能保证内存安全?Rust 把内存管理规则从"运行时检查"提前到"编译时检查"。编译器通过所有权系统静态分析每块内存的生命周期。违反规则就编译不过。代价是学习曲线陡峭。

1.3 适合与不适合的场景

  • 适合:系统编程、高性能服务、CLI 工具、WebAssembly、替代 C/C++
  • 不适合:快速原型/脚本(编译慢)、简单 CRUD(生态不如 Java/Go 成熟)

2 · 安装与工具链

2.1 安装

# Linux/macOS
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Windows:去 https://rustup.rs 下载 rustup-init.exe

rustc --version    # 编译器版本
cargo --version    # 包管理器版本

2.2 工具链组成

工具 作用 类比
rustc 编译器 gcc / clang
cargo 包管理器 + 构建工具 npm / pip
rustup 工具链版本管理器 nvm / pyenv
rustfmt 代码格式化 prettier
clippy linter eslint
TIP

📌 stable / beta / nightly:stable 每 6 周发布(生产用);nightly 每天构建含实验特性。用 rustup default stable 切换。

2.3 Cargo 常用命令

cargo new my_project          # 创建二进制项目
cargo new my_lib --lib        # 创建库
cargo build --release         # 编译(release,开启优化)
cargo run                     # 编译 + 运行
cargo check                   # 只做类型检查(比 build 快)
cargo test                    # 运行测试
cargo add serde               # 添加依赖
cargo fmt                     # 格式化
cargo clippy                  # lint 检查
TIP

📌 cargo check 比 cargo build 快:check 跳过 LLVM 代码生成阶段,只做类型和借用检查。写代码时用 check,最终才 build。

2.4 Cargo.toml

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

[profile.release]
opt-level = 3
lto = true
TIP

📌 Edition:每 3 年一个(2015/2018/2021),是语法改进集合,不是新语言版本。编译器保证向后兼容。


3 · 基础语法

3.1 变量与可变性

let x = 5;              // 不可变(默认!)
let mut y = 5;          // 可变,需显式 mut
// x = 6;               // 编译错误
y = 6;                  // OK

let z: i32 = 10;        // 显式类型标注
TIP

📌 为什么默认不可变?大多数变量不需要改变,默认不可变防止意外修改。不可变数据天然线程安全。

📌 变量遮蔽 (Shadowing) vs mut:遮蔽是声明新变量(同名),可以改变类型;mut 是同一变量修改值,类型不能变。

let s = "42";                          // &str
let s = s.parse::<i32>().unwrap();     // i32,遮蔽允许改类型

3.2 数据类型

// 整数:i32(默认) u8 u16 u32 u64 u128 usize isize
let a: i32 = -42;
let hex = 0xFF;
let big = 1_000_000;        // 下划线提高可读性

// 浮点:f64(默认) f32
let f: f64 = 3.14;

// 布尔
let t: bool = true;

// 字符:4 字节 Unicode
let ch: char = '中';

// 元组
let tup: (i32, f64, &str) = (500, 6.4, "hello");
let (x, y, z) = tup;        // 解构

// 数组(固定大小,栈上)
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10];        // 10 个 0
TIP

📌 整数默认 i32:即使 64 位系统也用 i32,对绝大多数计算足够,缓存友好。

📌 数组越界是 panic 不是 UB:Rust 安全代码中永远不产生未定义行为。越界 panic 是"安全的失败"。

整数溢出

let max = u8::MAX;        // 255
let wrapped = max.wrapping_add(1);    // 0,明确回绕
let checked = max.checked_add(1);     // None
let saturating = max.saturating_add(1); // 255,饱和

3.3 函数与表达式

fn add(a: i32, b: i32) -> i32 {
    a + b                   // 无分号 = 表达式 = 返回值
}

fn early_return(x: i32) -> i32 {
    if x < 0 { return -1; }
    x * 2
}
TIP

📌 语句 vs 表达式:语句不返回值(let x = 5;),表达式求值为值(5 + 3)。代码块和 if 都是表达式:

let y = { let z = 3; z + 1 };     // y = 4
let n = if cond { 1 } else { 0 }; // if 当三元运算符用

3.4 控制流

// loop(可 break 返回值)
let result = loop { counter += 1; if counter == 10 { break counter * 2; } };

// while
while n < 100 { n *= 2; }

// for(基于迭代器)
for i in 0..5 { }       // 0,1,2,3,4
for i in 0..=5 { }      // 0,1,2,3,4,5
for (i, v) in arr.iter().enumerate() { }  // 带索引

// match
match x {
    1 => println!("一"),
    2 | 3 => println!("二或三"),
    _ => println!("其他"),
}
TIP

📌 没有 C 风格 for(i=0;i<n;i++):容易 off-by-one。Rust 的 for in 基于迭代器,天然安全。需要索引用 .enumerate()。


4 · 所有权与借用

Rust 最核心、最独特、也最难的概念。

4.1 三条所有权规则

1. 每个值在任一时刻有且仅有一个「所有者」 2. 当所有者离开作用域,值被自动释放 3. 可以「借用」值(引用),但借用规则由编译器检查

图 2 \xb7 所有权与移动语义

4.2 移动 (Move)

let s1 = String::from("hello");
let s2 = s1;             // 所有权移动到 s2
// println!("{}", s1);   // s1 已被移动
println!("{}", s2);      // OK
TIP

📌 为什么 s1 不能用了?String 栈上存指针+长度+容量,指向堆数据。let s2 = s1 复制栈数据后两个变量指向同一块堆——如果都有效,各自离开作用域时都会释放堆 → double free。Rust 让 s1 失效(移动语义),编译期解决,零运行时开销。

📌 Clone:let s2 = s1.clone(); 深拷贝,两个都能用。

4.3 Copy — 栈数据自动复制

let x = 5;
let y = x;               // i32 实现了 Copy
println!("{} {}", x, y); // x 仍可用
TIP

📌 Copy 的条件:类型所有字段都是 Copy 的(无堆资源)。整数、浮点、布尔、字符、不可变引用都 Copy。String、Vec 不 Copy。

图 3 \xb7 借用规则

4.4 引用与借用

fn calc_len(s: &String) -> usize { s.len() }  // 借用,不获取所有权

let s1 = String::from("hello");
let len = calc_len(&s1);    // 借用 s1
println!("{}", s1);         // 仍可用

可变引用与借用规则

let mut s = String::from("hi");
let r1 = &s;                // 不可变引用 1
let r2 = &s;                // 不可变引用 2
println!("{} {}", r1, r2);  // r1, r2 最后一次使用

let r3 = &mut s;            // r1,r2 已不再使用(NLL)
r3.push_str("!");

// 规则:同一时刻要么一个 &mut,要么多个 &,不能共存
TIP

📌 为什么限制这么严格?防止迭代器失效:let first = &data[0]; data.push(4); 会让 Vec 可能重新分配内存,first 悬垂。借用规则在编译期阻止这种代码。

📌 NLL (Non-Lexical Lifetimes):引用在最后一次使用后就算"死"了,不必等到作用域结束。

悬垂引用

// 编译错误
fn dangle() -> &String {
    let s = String::from("hello");
    &s   // s 函数结束时释放,引用悬垂!
}
// 直接返回 String,转移所有权
fn no_dangle() -> String { String::from("hello") }

4.5 切片 (Slice)

let s = String::from("hello world");
let hello = &s[0..5];      // &str
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3];    // &[i32]
TIP

📌 String vs &str:String 拥有堆数据,可增长可修改。&str 是切片引用。函数参数优先用 &str(更通用,能接受 &String 和 &str)。


5 · 生命周期

图 4 \xb7 生命周期标注

5.1 为什么需要生命周期

// 编译器不知道返回的引用来自 x 还是 y
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
TIP

📌 生命周期标注的作用:告诉编译器"返回引用跟输入引用同寿",编译器据此检查调用方。生命周期不改变引用实际寿命,只是编译期约束。

5.2 生命周期标注

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

5.3 省略规则

1. 每个引用参数各自获得一个生命周期 2. 只有一个输入生命周期 → 赋给所有输出引用 3. 有 &self/&mut self → self 的生命周期赋给所有输出引用
// 省略后(规则 2 自动推导)
fn first_word(s: &str) -> &str { ... }

5.4 'static 与结构体中的生命周期

let s: &'static str = "hello";  // 字面量在只读数据段,程序运行期间始终有效

struct Excerpt<'a> {
    part: &'a str,    // 结构体持有引用时必须标注
}
TIP

📌 'static:表示引用至少跟程序一样长。不要滥用 'static 解决生命周期错误——说明设计可能有问题。


6 · 结构体与枚举

6.1 结构体

struct User { name: String, age: u32, active: bool }
let user = User { name: "Alice".into(), age: 30, active: true };
let user2 = User { age: 28, ..user };  // 更新语法

struct Point(i32, i32, i32);  // 元组结构体
struct Marker;                // 单元结构体(用于实现 trait)

6.2 方法 — impl 块

struct Rectangle { width: f64, height: f64 }

impl Rectangle {
    fn new(w: f64, h: f64) -> Self { Rectangle { width: w, height: h } }
    fn area(&self) -> f64 { self.width * self.height }           // 不可变借用
    fn scale(&mut self, f: f64) { self.width *= f; self.height *= f; }  // 可变借用
}

let mut r = Rectangle::new(10.0, 20.0);
println!("{}", r.area());  // 200
TIP

📌 self vs &self vs &mut self:&self 只读借用(最常见),&mut self 修改实例,self 获取所有权。能 &self 就不用 &mut self。

6.3 枚举 — 代数数据类型

enum Message {
    Quit,                         // 无数据
    Move { x: i32, y: i32 },     // 命名字段
    Write(String),                // 单值
    ChangeColor(i32, i32, i32),  // 元组
}
TIP

📌 Rust 枚举是 ADT:每个变体可携带不同类型和数量的数据。Option<T> 就是枚举,替代了 null:enum Option<T> { Some(T), None }。你必须显式处理 None,编译器不会让你忘记。

6.4 模式匹配

match msg {
    Message::Quit => println!("退出"),
    Message::Move { x, y } => println!("({}, {})", x, y),
    Message::Write(text) => println!("{}", text),
    _ => println!("其他"),
}
// match 必须穷尽,_ 是通配符

6.5 if let / while let

if let Some(x) = some_value { println!("{}", x); }
while let Some(top) = stack.pop() { println!("{}", top); }

6.6 解构

let Point { x, y } = p;           // 结构体解构
let (a, b, c) = (1, 2, 3);       // 元组解构
fn print(Point { x, y }: Point) { } // 函数参数解构

7 · 集合类型

7.1 Vec

let mut v: Vec<i32> = Vec::new();
v.push(1); v.push(2);
let v2 = vec![1, 2, 3];           // vec! 宏

let third = &v[2];                 // 越界 panic
let third = v.get(2);              // 越界返回 None

for val in &mut v { *val += 10; } // 可变遍历
TIP

📌 v[i] vs v.get(i):v[i] 越界 panic(确信索引合法时用)。v.get(i) 返回 Option(索引可能不合法时用)。

7.2 String

let mut s = String::new();
s.push_str("hello"); s.push(' ');
let s = format!("{} {}", "hello", "world");  // 不消耗参数
for c in "你好".chars() { }       // Unicode 字符遍历
TIP

📌 为什么 String 不能直接索引:UTF-8 编码下一个字符占 1-4 字节,s[0] 语义不明。用 chars() 按字符,bytes() 按字节。

7.3 HashMap

use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("Alice", 30);
map.entry("Bob").or_insert(25);   // 只在键不存在时插入
if let Some(age) = map.get("Alice") { println!("{}", age); }
TIP

📌 entry().or_insert():把"检查+插入"封装成原子操作,避免 TOCTOU 问题,且只查找一次。

7.4 其他集合

集合 用途
VecDeque<T> 双端队列
HashSet<T> 去重、集合运算
BTreeMap<K,V> 有序映射,范围查询
BinaryHeap<T> 优先队列

8 · 错误处理

图 5 \xb7 错误处理模型

8.1 两种错误

可恢复错误 → Result<T, E> (文件不存在、网络超时) 不可恢复错误 → panic! (数组越界、不变量被破坏)

8.2 panic!

panic!("崩溃了!");    // 打印信息,展开栈,退出
// 可设 RUST_BACKTRACE=1 看完整调用栈
TIP

📌 panic 时发生了什么:打印错误和调用栈 → 栈展开(逐层调用析构函数清理资源)→ 退出。可通过 panic = "abort" 设为直接退出。

📌 什么时候该 panic:程序处于不可恢复的不一致状态时。预期的错误用 Result。

8.3 Result 与 ? 运算符

fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;           // ? 自动传播错误
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}
TIP

📌 ? 的原理:Ok(x) 取出 x 继续;Err(e) 立即返回 Err(e)。还会自动做 From 类型转换。让错误传播像写正常代码一样自然——这是 Rust 错误处理体验远超 Go 的 if err != nil 的关键。

8.4 自定义错误类型

// 实际项目推荐用 thiserror(库)或 anyhow(应用)
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("IO 错误: {0}")]
    Io(#[from] io::Error),
    #[error("解析错误: {0}")]
    Parse(#[from] std::num::ParseIntError),
}

// anyhow(应用代码)
use anyhow::{Result, Context};
fn load_config(path: &str) -> Result<i32> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("读取 {} 失败", path))?;
    Ok(content.trim().parse()?)
}

8.5 unwrap 系列

result.unwrap();              // Err → panic
result.expect("应该成功");    // 同上但自定义信息
result.unwrap_or(0);          // Err → 默认值
result.unwrap_or_else(|e| 0); // Err → 计算默认值
TIP

📌 什么时候可以 unwrap:原型开发、确信不可能出错、测试代码。生产代码避免裸 unwrap。


9 · 泛型与 Trait

9.1 泛型

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut max = &list[0];
    for item in &list[1..] { if item > max { max = item; } }
    max
}
struct Pair<T> { x: T, y: T }
TIP

📌 泛型的零成本抽象:Rust 泛型是单态化——编译器为每个具体类型生成专用代码,运行时无泛型开销。与 Java 的类型擦除不同。

9.2 Trait

trait Animal {
    fn name(&self) -> String;
    fn sound(&self) -> String { String::from("(无声)") }  // 默认实现
}
struct Dog { name: String }
impl Animal for Dog {
    fn name(&self) -> String { self.name.clone() }
    fn sound(&self) -> String { String::from("汪汪") }
}
TIP

📌 孤儿规则:不能为外部类型实现外部 trait(至少 trait 或类型之一在你的 crate 中)。防止冲突。

9.3 Trait Bound / Derive / 动态分发

fn print<T: Display>(item: &T) { println!("{}", item); }
fn print2(item: &impl Display) { }  // 语法糖
// 动态分发
fn print_all_dyn(animals: &[Box<dyn Animal>]) { }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User { name: String, age: u32 }
TIP

📌 静态 vs 动态分发:静态(泛型)编译期单态化零开销但代码膨胀。动态(dyn Trait)运行时虚表查找,可混合不同类型。

9.4 关联类型

trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }

10 · 闭包与迭代器

10.1 闭包

let add = |a, b| a + b;
let f = move || { /* move 获取所有权 */ };
TIP

📌 闭包三种 Trait:Fn(不可变借用)、FnMut(可变借用)、FnOnce(获取所有权)。编译器按最小权限推断。返回闭包需 Box<dyn Fn()>。

10.2 迭代器适配器

let result: Vec<i32> = v.iter()
    .filter(|x| *x % 2 == 0).map(|x| x * 3).collect();
let sum: i32 = v.iter().fold(0, |acc, x| acc + x);
v.iter().take(3).skip(2).any(|x| *x > 3).max().sum::<i32>();
TIP

📌 零成本抽象:迭代器惰性执行,map/filter 不产生中间集合,编译器优化为等价手写循环。


11 · 智能指针

图 7 \xb7 智能指针体系

11.1 Box / Rc / RefCell / Arc / Mutex

let b = Box::new(5);                    // 堆分配,独占所有权
let a = Rc::new(String::from("hi"));    // 引用计数(单线程)
let c = Arc::new(vec![1, 2, 3]);        // 原子引用计数(多线程)
let data = RefCell::new(vec![1]);       // 运行时借用检查(单线程)
let counter = Arc::new(Mutex::new(0));  // 多线程互斥
TIP

📌 标准模式:Rc<RefCell<T>> = 单线程共享可变;Arc<Mutex<T>> = 多线程共享可变。Deref 自动解引用,Drop 自动释放(RAII)。


12 · 并发编程

图 6 \xb7 并发安全模型

12.1 线程 / Send / Sync / Channel

let handle = thread::spawn(move || { println!("{:?}", data); });
handle.join().unwrap();

let (tx, rx) = mpsc::channel();
thread::spawn(move || { tx.send("hi").unwrap(); });
for r in rx { println!("{}", r); }
TIP

📌 Send/Sync 是无畏并发核心:编译器编译期检查——Rc 不是 Send(非原子),Arc 是。所有字段 Send 则类型自动 Send。编译器帮你消除数据竞争。


13 · 模块系统

mod garden { pub mod vegetables { pub struct Potato { pub size: f64 } } }
use garden::vegetables::Potato;
use std::{collections::HashMap, fs::File};
src/main.rs → crate 根 | src/lib.rs → 库根 src/garden.rs → mod garden | src/garden/vegetables.rs → mod vegetables

14 · 宏

macro_rules! say_hello {
    () => { println!("Hello!"); };
    ($name:expr) => { println!("Hello, {}!", $name); };
}
TIP

📌 宏 vs 函数:宏编译期展开,可变参数,操作 AST。声明宏(macro_rules!)和过程宏(derive/属性/函数式)。


15 · 异步编程

#[tokio::main]
async fn main() {
    let r = fetch_data("https://example.com").await;
    let (r1, r2) = tokio::join!(fetch("a"), fetch("b"));  // 并发
}
TIP

📌 async 返回 Future 不执行:需 runtime(tokio)驱动。async fn 编译成状态机,每个 .await 是状态切换点。零成本——无栈协程,大小编译期确定。


16 · Unsafe Rust 与 FFI

unsafe {
    // 1.解引用裸指针 2.调用unsafe函数 3.实现unsafe trait
    // 4.访问可变static 5.访问union字段
}

extern "C" { fn abs(x: i32) -> i32; }  // FFI 调用 C
#[no_mangle] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }  // 导出给 C
TIP

📌 Unsafe 不关闭安全检查:只是告诉编译器"我保证这五个操作安全"。其他规则仍生效。用于 FFI、性能优化、底层安全抽象。


附录 A · Cargo 速查

命令 作用
cargo new / build / run / check / test 创建/编译/运行/检查/测试
cargo build --release release 优化编译
cargo add / fmt / clippy / doc 依赖/格式化/lint/文档
cargo bench / tree / update 基准/依赖树/更新

附录 B · 测试

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn test_add() { assert_eq!(add(1, 2), 3); }
    #[should_panic] #[test] fn test_panic() { panic!(); }
}

附录 C · 设计模式

// Newtype:用元组结构体创建新类型
struct Meters(f64);
impl Meters { fn to_feet(&self) -> f64 { self.0 * 3.28084 } }

// Builder:链式构建复杂对象
let cmd = CommandBuilder::new().program("ls").arg("-la").build()?;