所有权与借用
学习内容: 为什么 Rust 有所有权(无 GC!)、移动语义 vs Python 的引用计数、 借用(
&和&mut)、生命周期基础,以及智能指针(Box、Rc、Arc)。难度: 🟡 中级
这是对 Python 开发者最困难的概念。在 Python 中,你从不考虑谁”拥有”数据——垃圾回收器处理它。 在 Rust 中,每个值有且只有一个所有者,编译器在编译期追踪这一点。
Python:到处都是共享引用
Section titled “Python:到处都是共享引用”# Python — 一切都是引用,gc 负责清理a = [1, 2, 3]b = a # b 和 a 指向同一个列表b.append(4)print(a) # [1, 2, 3, 4] — 惊讶!a 也被改了
# 谁拥有这个列表?a 和 b 都引用它。# 当没有引用残留时,垃圾回收器释放它。# 你从不考虑这个。Rust:单一所有权
Section titled “Rust:单一所有权”// Rust — 每个值有且只有一个所有者let a = vec![1, 2, 3];let b = a; // 所有权从 a 移动到 b// println!("{:?}", a); // 编译错误:值在移动后被使用
// a 不再存在。b 是唯一的所有者。println!("{:?}", b); // [1, 2, 3]
// 当 b 超出作用域时,Vec 被释放。确定性的。无 GC。三条所有权规则
Section titled “三条所有权规则”1. 每个值有且只有一个所有者变量。2. 当所有者超出作用域时,值被丢弃(释放)。3. 所有权可以转移(移动)但不能复制(除非实现 Clone)。移动语义——对 Python 最大的冲击
Section titled “移动语义——对 Python 最大的冲击”# Python — 赋值复制引用,不复制数据def process(data): data.append(42) # 原始列表被修改了!
my_list = [1, 2, 3]process(my_list)print(my_list) # [1, 2, 3, 42] — 被 process 修改了!// Rust — 传递给函数会移动所有权(对于非 Copy 类型)fn process(mut data: Vec<i32>) -> Vec<i32> { data.push(42); data # 必须返回才能把所有权拿回来!}
let my_vec = vec![1, 2, 3];let my_vec = process(my_vec); # 所有权移入又移出println!("{:?}", my_vec); # [1, 2, 3, 42]
// 更好的方式——借用而非移动:fn process_borrowed(data: &mut Vec<i32>) { data.push(42);}
let mut my_vec = vec![1, 2, 3];process_borrowed(&mut my_vec); # 临时借出println!("{:?}", my_vec); # [1, 2, 3, 42] — 仍然是我们自己的所有权可视化
Section titled “所有权可视化”Python: Rust:
a ──────┐ a ──→ [1, 2, 3] ├──→ [1, 2, 3] b ──────┘ After: let b = a;
(a and b share one object) a (invalid, moved) (refcount = 2) b ──→ [1, 2, 3] (only b owns the data)
del a → refcount = 1 drop(b) → data freed del b → refcount = 0 → freed (deterministic, no GC)stateDiagram-v2 state "Python (Reference Counting)" as PY { [*] --> a_owns: a = [1,2,3] a_owns --> shared: b = a shared --> b_only: del a (refcount 2→1) b_only --> freed: del b (refcount 1→0) note right of shared: Both a and b point<br/>to the SAME object } state "Rust (Ownership Move)" as RS { [*] --> a_owns2: let a = vec![1,2,3] a_owns2 --> b_owns: let b = a (MOVE) b_owns --> freed2: b goes out of scope note right of b_owns: a is INVALID after move<br/>Compile error if used }移动语义 vs 引用计数
Section titled “移动语义 vs 引用计数”Copy vs Move
Section titled “Copy vs Move”// 简单类型(整数、浮点数、布尔、字符)是复制,不是移动let x = 42;let y = x; // x 复制到 y(两者都有效)println!("{x} {y}"); // 42 42
// 堆分配类型(String、Vec、HashMap)是移动let s1 = String::from("hello");let s2 = s1; // s1 移动到 s2// println!("{s1}"); // 错误:值在移动后被使用
// 要显式复制堆数据,使用 .clone()let s1 = String::from("hello");let s2 = s1.clone(); // 深复制println!("{s1} {s2}"); // hello hello(两者都有效)Python 开发者的心智模型
Section titled “Python 开发者的心智模型”Python: Rust:───────── ─────int, float, bool Copy types (i32, f64, bool, char)→ shared refs to immutable → 按位复制到赋值 objects (no real copy) (总是独立值) (注:Python 缓存小整数;Rust 复制总是可预测的)
list, dict, str Move types (Vec, HashMap, String)→ shared reference → 所有权转移(不同的行为!)→ gc cleans up → 所有者丢弃数据→ clone with list(x) → clone with x.clone() or copy.deepcopy(x)Python 共享模型何时导致 Bug
Section titled “Python 共享模型何时导致 Bug”# Python — 意外的别名def remove_duplicates(items): seen = set() result = [] for item in items: if item not in seen: seen.add(item) result.append(item) return result
original = [1, 2, 2, 3, 3, 3]alias = original # 别名,不是复制unique = remove_duplicates(alias)# original 仍然是 [1, 2, 2, 3, 3, 3] — 但只是因为我们没修改输入# 如果 remove_duplicates 修改了输入,original 也会受影响use std::collections::HashSet;
// Rust — 所有权防止意外别名fn remove_duplicates(items: &[i32]) -> Vec<i32> { let mut seen = HashSet::new(); items.iter() .filter(|&&item| seen.insert(item)) .copied() .collect()}
let original = vec![1, 2, 2, 3, 3, 3];let unique = remove_duplicates(&original); // 借用 — 无法修改// original 保证不变 — 编译器通过 & 防止了修改借用与生命周期
Section titled “借用与生命周期”借用 = 借出一本书
Section titled “借用 = 借出一本书”Think of ownership like a physical book:
Python: Everyone has a photocopy (shared references + GC)Rust: One person owns the book. Others can: - &book = look at it (immutable borrow, many allowed) - &mut book = write in it (mutable borrow, exclusive) - book = give it away (move)flowchart TD R["Borrowing Rules"] --> IMM["✅ Many &T<br/>(shared/immutable)"] R --> MUT["✅ One &mut T<br/>(exclusive/mutable)"] R --> CONFLICT["❌ &T + &mut T<br/>(NEVER at same time)"] IMM --> SAFE["Multiple readers, safe"] MUT --> SAFE2["Single writer, safe"] CONFLICT --> ERR["Compile error!"] style IMM fill:#d4edda style MUT fill:#d4edda style CONFLICT fill:#f8d7da style ERR fill:#f8d7da,stroke:#dc3545// 规则 1:你可以有多个不可变借用,或一个可变借用(不能同时有)
let mut data = vec![1, 2, 3];
// 多个不可变借用 — 可以let a = &data;let b = &data;println!("{:?} {:?}", a, b); // 可以
// 可变借用 — 必须是独占的let c = &mut data;c.push(4);// println!("{:?}", a); // 错误:可变借用存在时不能使用不可变借用
// 这在编译期防止了数据竞争!// Python 没有等价物 — 这就是为什么 Python 字典在迭代时被修改会在运行时崩溃。生命周期——简要介绍
Section titled “生命周期——简要介绍”// 生命周期回答:"这个引用活多久?"// 通常编译器会推断。你很少需要显式写。
// 简单情况 — 编译器处理:fn first_word(s: &str) -> &str { s.split_whitespace().next().unwrap_or("")}// 编译器知道:返回的 &str 与输入的 &str 生存期相同
// 当你需要显式生命周期时(很少):fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b }}// 'a 表示:"返回值与两个输入活得一样久"对 Python 开发者:开始不用担心生命周期。编译器会在你需要时告诉你, 而且 95% 的情况它会自动推断。 把生命周期标注想象成当编译器自己搞不定时,你给它的提示。
当单一所有权限制太多时,Rust 提供了智能指针。 这些更接近 Python 的引用模型——但是显式的、可选的。
// Box<T> — 堆分配,单一所有者(类似 Python 的普通分配)let boxed = Box::new(42); // 堆分配的 i32
// Rc<T> — 引用计数(类似 Python 的 refcount!)use std::rc::Rc;let shared = Rc::new(vec![1, 2, 3]);let clone1 = Rc::clone(&shared); // 增加引用计数let clone2 = Rc::clone(&shared); // 增加引用计数// 三个都指向同一个 Vec。全部被丢弃时,Vec 被释放。// 类似于 Python 的引用计数,但 Rc 不处理循环——// 使用 Weak<T> 打破循环(Python 的 GC 自动处理循环)
// Arc<T> — 原子引用计数(多线程代码的 Rc)use std::sync::Arc;let thread_safe = Arc::new(vec![1, 2, 3]);// 跨线程共享时使用 Arc(Rc 是单线程的)
// RefCell<T> — 运行时借用检查(类似 Python 的"随便改"模型)use std::cell::RefCell;let cell = RefCell::new(42);*cell.borrow_mut() = 99; // 运行时可变借用(双重借用时 panic)何时使用哪种
Section titled “何时使用哪种”| 智能指针 | Python 类比 | 使用场景 |
|---|---|---|
Box<T> | 普通分配 | 大数据、递归类型、trait 对象 |
Rc<T> | Python 默认 refcount | 共享所有权、单线程 |
Arc<T> | 线程安全 refcount | 共享所有权、多线程 |
RefCell<T> | Python”随便改” | 内部可变性(逃生舱) |
Rc<RefCell<T>> | Python 普通对象模型 | 共享 + 可变(图形结构) |
核心洞见:
Rc<RefCell<T>>给你 Python 似的语义(共享、可变数据), 但你必须显式选择加入。Rust 默认(所有权、移动)更快,避免了引用计数开销。 对于有循环的图形结构,使用Weak<T>打破引用循环—— 与 Python 不同,Rust 的Rc没有循环收集器。
另请参阅:第 13 章 — 并发 介绍
Arc<Mutex<T>>用于多线程共享状态。
练习:发现借用检查器错误(点击展开)
挑战:以下代码有 3 个借用检查器错误。找出每一个并修复,不要使用 .clone():
fn main() { let mut names = vec!["Alice".to_string(), "Bob".to_string()]; let first = &names[0]; names.push("Charlie".to_string()); println!("First: {first}");
let greeting = make_greeting(names[0]); println!("{greeting}");}
fn make_greeting(name: String) -> String { format!("Hello, {name}!")}答案
fn main() { let mut names = vec!["Alice".to_string(), "Bob".to_string()]; let first = &names[0]; println!("First: {first}"); // 在修改前使用借用 names.push("Charlie".to_string()); // 现在安全了 — 没有活跃的不可变借用
let greeting = make_greeting(&names[0]); // 传引用,不传所有权 println!("{greeting}");}
fn make_greeting(name: &str) -> String { // 接受 &str,不是 String format!("Hello, {name}!")}修复的错误:
- 不可变借用 + 修改:
first借用了names,然后push修改了它。修复:在 push 前使用first。 - 从 Vec 移出:
names[0]尝试从 Vec 移出一个 String(不允许)。修复:用&names[0]借用。 - 函数获取所有权:
make_greeting(String)消耗了值。修复:改为接受&str。