为什么
所有权三大规则(必须背熟)
- 每个值(value)在任意时刻都有且只有一个所有者(owner)
- 当所有者离开作用域(scope),值会被自动释放(drop)
- 值的所有权可以转移(move),但转移后原变量不能再用
上诉三个规则解决的问题:不会出现悬垂指针、重复释放、数据竞争等问题
常见场景
作用域与自动释放
fn main() {
{
let s = String::from("hello");
// s 在这里拥有这段堆内存(String 的内容在堆上)
println!("{}", s);
} // <- 作用域结束:s 被 drop,堆内存自动释放
// println!("{}", s); // ❌ 编译错误:s 已经不在作用域内
}
Move:所有权转移(最常见)
String 这类“拥有堆内存”的类型,默认赋值/传参会 move(转移所有权),不是拷贝
fn main() {
let s1 = String::from("hello");
let s2 = s1;
// ↑ 所有权从 s1 转移到 s2
// s1 现在“失效”,避免同一块堆内存被释放两次
// println!("{}", s1); // ❌ 编译错误:borrow of moved value
println!("{}", s2); // ✅
}
为什么这么设计:如果允许 s1、s2 同时“以为自己拥有同一块堆内存”,作用域结束时会,发生double free。Rust 直接在编译期禁止。
Copy:栈上按位复制(不会 move)
像整数、布尔、浮点、char 以及它们的简单组合(只要实现了 Copy)赋值时是 copy。
fn main() {
let a = 10; // i32 是 Copy
let b = a; // 发生按位复制(copy)
println!("{a} {b}"); // ✅ a 依然可用
}
哪些常见类型是 Copy?
- 数字类型:i32, u64, f32...
- bool, char
- (i32, bool) 这种成员全是 Copy 的 tuple(元组)
- &T 引用本身通常也是 Copy(注意:复制的是“引用”,不是复制数据)
Clone:显式深拷贝(堆数据复制)
如果你确实想复制 String 的堆内容,用 clone()。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
// ↑ 深拷贝:堆上复制一份新数据,s1 和 s2 各自拥有自己的堆内存
println!("{s1} {s2}"); // ✅ 都可用
}
经验:默认 move,想复制就显式 clone,避免不小心带来性能成本。
函数传参:move vs borrow(借用)
传入 String:默认 move
fn take(s: String) {
// s 拥有传进来的 String
println!("{s}");
} // <- s drop,释放堆内存
fn main() {
let s1 = String::from("hi");
take(s1); // move 进去
// println!("{s1}"); // ❌ s1 已经被 move
}
传入 &String 或 &str:借用(不转移所有权)
fn read_only(s: &String) {
// s 是对 String 的不可变借用(read-only)
println!("{s}");
}
fn main() {
let s1 = String::from("hi");
read_only(&s1); // 只借用
println!("{s1}"); // ✅ 所有权仍在 s1
}
// 更推荐写成 &str(更通用):
fn read_only_str(s: &str) {
println!("{s}");
}
fn main() {
let s = String::from("hello");
read_only_str(&s); // &String 会自动解引用成 &str(Deref coercion)
read_only_str("world"); // 字面量本身就是 &str
}
借用规则(Borrowing Rules)
核心两条(同一时间、同一份数据):
1. 可以有任意多个不可变借用(&T)
2. 或者只能有一个可变借用(&mut T)
3. 可变借用与不可变借用不能同时存在
多个不可变借用:允许
fn main() {
let s = String::from("hello");
let r1 = &s; // 不可变借用
let r2 = &s; // 另一个不可变借用
println!("{r1} {r2}"); // ✅
}
可变借用:同一时间只能一个
fn main() {
let mut s = String::from("hello");
let r1 = &mut s; // 可变借用:独占
r1.push_str("!");
println!("{r1}");
// let r2 = &mut s; // ❌ 如果 r1 还在使用期间,再借用会报错
}
不可变借用与可变借用不能同时存在
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可变借用开始
// let r2 = &mut s; // ❌ 同时想要可变借用:不允许
println!("{r1}"); // r1 用到这里为止
}
返回值与所有权:move out / 借用返回
返回 String:把所有权交给调用者
fn make() -> String {
let s = String::from("made");
s // 返回:所有权 move 给调用者;这里不 drop
}
fn main() {
let s = make();
println!("{s}");
}
想返回引用:必须保证引用指向的数据活得足够久(生命周期)
下面这个是典型不允许的:
fn bad_ref() -> &str {
let s = String::from("oops");
// &s[..] 指向 s 的内部数据,但 s 函数结束就被 drop
// 返回它会产生悬垂引用
// &s[..]
todo!()
}
正确方式通常是:
返回拥有所有权的值(String),或让引用来自调用者传入的数据(生命周期参数)
fn first_word<'a>(s: &'a str) -> &'a str {
// 返回的切片引用来自参数 s,因此生命周期跟着 s
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
let s = String::from("hello world");
let w = first_word(&s);
println!("{w}");
}
切片(Slice)与所有权:引用视图,不拥有数据
fn main() {
let s = String::from("hello");
let part = &s[0..2]; // &str 切片:借用 s 的一部分
println!("{part}");
// s.push('!');
// ❌ 如果 part 仍在使用,push 可能触发扩容并搬移堆内存,
// 导致 part 悬垂
}
容器与所有权:Vec / String 里装的东西是谁的
Vec 拥有它里面的 T
fn main() {
let v = vec![String::from("a"), String::from("b")];
// v 拥有两个 String
// let x = v[0]; // ❌ 不能把 String 直接 move 出去
//(索引返回引用视图的语义)
let x = v[0].clone(); // ✅ 如果要拿一个独立 String,用 clone
println!("{x}");
// 更常见:用引用访问
let r = &v[1];
println!("{r}");
}
如果你确实要“拿走”元素,通常用 remove / pop(会改变 Vec)
fn main() {
let mut v = vec![String::from("a"), String::from("b")];
let x = v.remove(0); // ✅ move 出元素,同时 Vec 调整
println!("{x}");
}
结构体字段与 move:部分 move(很常见的坑)
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
let u = User {
name: String::from("Alice"),
age: 20,
};
let name = u.name; // move 出 name 字段(String 非 Copy)
// println!("{u:?}"); // ❌ u 已被“部分 move”,整体不能再用
println!("{name}");
// u.age 其实是 Copy,但由于 u 已部分 move,
// 直接用 u.age 也会受限制
}
解决思路:
- 要么借用:let name = &u.name;
- 要么 clone:let name = u.name.clone();
- 要么用解构并只 move 需要的字段(并接受 u 之后不可用)
共享所有权(进阶):Rc/Arc + 内部可变性 RefCell/Mutex
本质
把“谁负责释放这个值”从编译期的唯一拥有者,改成运行时的引用计数/锁管理。
实现
当你需要“多个地方同时拥有同一份数据”时,基础所有权规则不够,需要用标准库提供的
智能指针:
- 单线程共享所有权:Rc
- 多线程共享所有权:Arc
- 运行时借用检查(单线程):RefCell
- 多线程可变共享:Mutex / RwLock
Rc:单线程共享所有权(引用计数)
- - Rc::clone(&rc)只增加引用计数,不复制数据
- - 不能跨线程:Rc 不是 Send/Sync
- - 只解决“多个 owner”,不解决可变共享
use std::rc::Rc;
fn main() {
let s = Rc::new(String::from("shared"));
let a = Rc::clone(&s); // 计数 +1
let b = Rc::clone(&s); // 计数 +1
// Rc<T> 支持像引用一样解引用读取
println!("{}", a);
println!("{}", b);
// 当 a/b/s 都离开作用域后,引用计数归零,底层 String 才 drop
}
Rc + RefCell:单线程“可变共享”(经典组合)
因为 Rc 只能给出 &T(共享引用),你拿不到 &mut T;要想在多 owner 情况下修改数据,需要内部可变性:
- RefCell:把借用检查从编译期推迟到运行时,规则仍然是:任意多个不可变借用或一个可变借用;但违反时会 panic
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
// 多个 owner 共享同一个 Vec<i32>,并允许修改
let data: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(vec![1, 2, 3]));
let a = Rc::clone(&data);
let b = Rc::clone(&data);
// 可变借用:borrow_mut() 返回一个“借用守卫”,离开作用域才释放可变借用
{
let mut v = a.borrow_mut(); // 运行时检查:此刻必须没有其它借用
v.push(4);
} // v 在这里 drop,可变借用结束
// 不可变借用:borrow()
{
let v = b.borrow(); // 运行时检查:此刻必须没有可变借用
println!("{:?}", *v);
}
// 典型坑:同一作用域里借用守卫没 drop,就再借用会 panic
// let _r1 = data.borrow_mut();
// let _r2 = data.borrow_mut(); // ❌ 运行时 panic: already borrowed
}
选型建议(单线程):
- 只读共享:Rc
- 共享且要改:Rc<RefCell>
- 只改“Copy 小值”(如计数):Rc<Cell>(比 RefCell 更轻)
循环引用:Rc 的大坑,用 Weak 解决
Rc 是引用计数:如果 A 持有 B,B 又持有 A(强引用),计数永远不归零 → 内存泄漏(不会 drop)。
解决:把“指回去”的那条边做成 弱引用Weak。
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
// parent 不拥有孩子,避免环:Weak 不增加强计数
parent: RefCell<Weak<Node>>,
// children 拥有子节点:Rc 增加强计数
children: RefCell<Vec<Rc<Node>>>,
name: String,
}
fn main() {
let root = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
name: "root".into(),
});
let child = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
name: "child".into(),
});
// 建立 root -> child(强引用)
root.children.borrow_mut().push(Rc::clone(&child));
// 建立 child -> root(弱引用),避免环
*child.parent.borrow_mut() = Rc::downgrade(&root);
// 需要访问 parent 时:upgrade() 把 Weak 尝试变成 Rc
if let Some(p) = child.parent.borrow().upgrade() {
println!("parent = {}", p.name);
}
}
要点:
- Weak::upgrade() 返回 Option<Rc>:因为 parent 可能已经释放了
- 图结构(尤其双向边)几乎必用 Weak 来断环
Arc:多线程共享所有权(原子引用计数)
Arc 和 Rc 类似,但引用计数是原子的,可跨线程。
- Arc本身线程安全,但 T 是否能在线程间共享还要看 T: Send + Sync
- 仍然只解决“多个 owner”,不解决可变共享
use std::sync::Arc;
use std::thread;
fn main() {
let s = Arc::new(String::from("hello"));
let t1 = {
let s = Arc::clone(&s);
thread::spawn(move || {
println!("{}", s);
})
};
let t2 = {
let s = Arc::clone(&s);
thread::spawn(move || {
println!("{}", s);
})
};
t1.join().unwrap();
t2.join().unwrap();
}
Arc<Mutex>:多线程“可变共享”(最常见)
- Mutex:同一时刻只允许一个线程修改/读取(独占)
- lock() 返回 MutexGuard,它 drop 时自动解锁(RAII)
- 典型坑:死锁、锁持有太久、poisoning(中毒)
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u64));
let mut handles = vec![];
for _ in 0..4 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..100_000 {
// 加锁(可能阻塞)
let mut guard = counter.lock().unwrap();
*guard += 1;
// guard 在这里离开作用域自动解锁
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("counter = {}", *counter.lock().unwrap());
}
实战建议:
- 把临界区写小:尽量让 MutexGuard 尽快 drop(用 {} 块包起来)
- 多把锁要固定顺序上锁,避免死锁
- unwrap() 在锁中毒时会 panic;更稳妥是处理 PoisonError
Arc<RwLock>:读多写少场景
- 多个读锁可并发,写锁独占
- 读多写少常更快;但写竞争时也可能更慢(具体看负载)
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let r = {
let data = Arc::clone(&data);
thread::spawn(move || {
let guard = data.read().unwrap(); // 共享读锁
println!("read = {:?}", *guard);
})
};
let w = {
let data = Arc::clone(&data);
thread::spawn(move || {
let mut guard = data.write().unwrap(); // 独占写锁
guard.push(4);
})
};
r.join().unwrap();
w.join().unwrap();
}
无锁共享:Arc<Atomic*>(计数/标志位等)
当共享的数据是简单数值/状态机片段时,用原子类型避免锁开销:
- AtomicUsize, AtomicBool 等
- 需要理解内存序(Ordering);不确定就先用 SeqCst(简单但可能慢)
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
use std::thread;
fn main() {
let n = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..4 {
let n = Arc::clone(&n);
handles.push(thread::spawn(move || {
for _ in 0..100_000 {
n.fetch_add(1, Ordering::SeqCst);
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("n = {}", n.load(Ordering::SeqCst));
}
总结(选型速记)
- 单线程、只读共享:Rc
- 单线程、共享且要改:Rc<RefCell>(或 Rc<Cell>)
- 多线程、只读共享:Arc
- 多线程、共享且要改:Arc<Mutex>(简单通用)或 Arc<RwLock>(读多写少)
- 多线程、简单计数/标志:Arc<Atomic*>
- 有双向/环状引用:Weak 断环(Rc/Arc 都适用:Weak / sync::Weak)