一份覆盖完整知识点的 Rust 学习笔记。重点讲"为什么这么设计"(费曼式 📌 讲解),配代码示例。
图表在
/Rust学习笔记/diagrams/目录。图注名即原图文件名,看图注就能直接找到原图用 draw.io 编辑。
📌 打个比方:C/C++ 像给你一把没保险的枪——威力大但容易走火。Java/Go 像配了一个保镖(GC)——安全但有开销。Rust 像一把带保险的枪——编译器在编译时检查你每次扣扳机是否安全,通过就运行时不会走火。保险本身不消耗子弹(零成本)。
| 问题 | C/C++ | Java/Go | Rust |
|---|---|---|---|
| 内存安全 | 手动管理(易出错) | GC 自动回收 | 编译期所有权检查 |
| 线程安全 | 自行保证 | 运行时检查 | 编译期 Send/Sync |
| 性能 | 极快 | GC 带来开销 | 零成本抽象,与 C/C++ 持平 |
| 学习曲线 | 中等 | 低 | 高 |
📌 为什么没有 GC 也能保证内存安全?Rust 把内存管理规则从"运行时检查"提前到"编译时检查"。编译器通过所有权系统静态分析每块内存的生命周期。违反规则就编译不过。代价是学习曲线陡峭。
| 工具 | 作用 | 类比 |
|---|---|---|
rustc |
编译器 | gcc / clang |
cargo |
包管理器 + 构建工具 | npm / pip |
rustup |
工具链版本管理器 | nvm / pyenv |
rustfmt |
代码格式化 | prettier |
clippy |
linter | eslint |
📌 stable / beta / nightly:stable 每 6 周发布(生产用);nightly 每天构建含实验特性。用 rustup default stable 切换。
📌 cargo check 比 cargo build 快:check 跳过 LLVM 代码生成阶段,只做类型和借用检查。写代码时用 check,最终才 build。
📌 Edition:每 3 年一个(2015/2018/2021),是语法改进集合,不是新语言版本。编译器保证向后兼容。
📌 为什么默认不可变?大多数变量不需要改变,默认不可变防止意外修改。不可变数据天然线程安全。
📌 变量遮蔽 (Shadowing) vs mut:遮蔽是声明新变量(同名),可以改变类型;mut 是同一变量修改值,类型不能变。
📌 整数默认 i32:即使 64 位系统也用 i32,对绝大多数计算足够,缓存友好。
📌 数组越界是 panic 不是 UB:Rust 安全代码中永远不产生未定义行为。越界 panic 是"安全的失败"。
📌 语句 vs 表达式:语句不返回值(let x = 5;),表达式求值为值(5 + 3)。代码块和 if 都是表达式:
📌 没有 C 风格 for(i=0;i<n;i++):容易 off-by-one。Rust 的 for in 基于迭代器,天然安全。需要索引用 .enumerate()。
Rust 最核心、最独特、也最难的概念。
📌 为什么 s1 不能用了?String 栈上存指针+长度+容量,指向堆数据。let s2 = s1 复制栈数据后两个变量指向同一块堆——如果都有效,各自离开作用域时都会释放堆 → double free。Rust 让 s1 失效(移动语义),编译期解决,零运行时开销。
📌 Clone:let s2 = s1.clone(); 深拷贝,两个都能用。
📌 Copy 的条件:类型所有字段都是 Copy 的(无堆资源)。整数、浮点、布尔、字符、不可变引用都 Copy。String、Vec 不 Copy。
📌 为什么限制这么严格?防止迭代器失效:let first = &data[0]; data.push(4); 会让 Vec 可能重新分配内存,first 悬垂。借用规则在编译期阻止这种代码。
📌 NLL (Non-Lexical Lifetimes):引用在最后一次使用后就算"死"了,不必等到作用域结束。
📌 String vs &str:String 拥有堆数据,可增长可修改。&str 是切片引用。函数参数优先用 &str(更通用,能接受 &String 和 &str)。
📌 生命周期标注的作用:告诉编译器"返回引用跟输入引用同寿",编译器据此检查调用方。生命周期不改变引用实际寿命,只是编译期约束。
📌 'static:表示引用至少跟程序一样长。不要滥用 'static 解决生命周期错误——说明设计可能有问题。
📌 self vs &self vs &mut self:&self 只读借用(最常见),&mut self 修改实例,self 获取所有权。能 &self 就不用 &mut self。
📌 Rust 枚举是 ADT:每个变体可携带不同类型和数量的数据。Option<T> 就是枚举,替代了 null:enum Option<T> { Some(T), None }。你必须显式处理 None,编译器不会让你忘记。
📌 v[i] vs v.get(i):v[i] 越界 panic(确信索引合法时用)。v.get(i) 返回 Option(索引可能不合法时用)。
📌 为什么 String 不能直接索引:UTF-8 编码下一个字符占 1-4 字节,s[0] 语义不明。用 chars() 按字符,bytes() 按字节。
📌 entry().or_insert():把"检查+插入"封装成原子操作,避免 TOCTOU 问题,且只查找一次。
| 集合 | 用途 |
|---|---|
VecDeque<T> |
双端队列 |
HashSet<T> |
去重、集合运算 |
BTreeMap<K,V> |
有序映射,范围查询 |
BinaryHeap<T> |
优先队列 |
📌 panic 时发生了什么:打印错误和调用栈 → 栈展开(逐层调用析构函数清理资源)→ 退出。可通过 panic = "abort" 设为直接退出。
📌 什么时候该 panic:程序处于不可恢复的不一致状态时。预期的错误用 Result。
📌 ? 的原理:Ok(x) 取出 x 继续;Err(e) 立即返回 Err(e)。还会自动做 From 类型转换。让错误传播像写正常代码一样自然——这是 Rust 错误处理体验远超 Go 的 if err != nil 的关键。
📌 什么时候可以 unwrap:原型开发、确信不可能出错、测试代码。生产代码避免裸 unwrap。
📌 泛型的零成本抽象:Rust 泛型是单态化——编译器为每个具体类型生成专用代码,运行时无泛型开销。与 Java 的类型擦除不同。
📌 孤儿规则:不能为外部类型实现外部 trait(至少 trait 或类型之一在你的 crate 中)。防止冲突。
📌 静态 vs 动态分发:静态(泛型)编译期单态化零开销但代码膨胀。动态(dyn Trait)运行时虚表查找,可混合不同类型。
📌 闭包三种 Trait:Fn(不可变借用)、FnMut(可变借用)、FnOnce(获取所有权)。编译器按最小权限推断。返回闭包需 Box<dyn Fn()>。
📌 零成本抽象:迭代器惰性执行,map/filter 不产生中间集合,编译器优化为等价手写循环。
📌 标准模式:Rc<RefCell<T>> = 单线程共享可变;Arc<Mutex<T>> = 多线程共享可变。Deref 自动解引用,Drop 自动释放(RAII)。
📌 Send/Sync 是无畏并发核心:编译器编译期检查——Rc 不是 Send(非原子),Arc 是。所有字段 Send 则类型自动 Send。编译器帮你消除数据竞争。
📌 宏 vs 函数:宏编译期展开,可变参数,操作 AST。声明宏(macro_rules!)和过程宏(derive/属性/函数式)。
📌 async 返回 Future 不执行:需 runtime(tokio)驱动。async fn 编译成状态机,每个 .await 是状态切换点。零成本——无栈协程,大小编译期确定。
📌 Unsafe 不关闭安全检查:只是告诉编译器"我保证这五个操作安全"。其他规则仍生效。用于 FFI、性能优化、底层安全抽象。
| 命令 | 作用 |
|---|---|
cargo new / build / run / check / test |
创建/编译/运行/检查/测试 |
cargo build --release |
release 优化编译 |
cargo add / fmt / clippy / doc |
依赖/格式化/lint/文档 |
cargo bench / tree / update |
基准/依赖树/更新 |