Rust智能指针详解
Rust 智能指针详解
智能指针可以管理值的所有权、位置或生命周期。这里也一并介绍常与智能指针配合的内部可变性和同步容器:Cell<T>、RefCell<T>、Mutex<T>、RwLock<T>;它们本身并非严格意义上的指针。下列代码块各自独立,可分别保存为 main.rs 运行。
| 类型 | 主要用途 | 典型组合 |
|---|---|---|
Box<T> | 堆分配、递归类型、特征对象 | Box<dyn Trait> |
Rc<T> | 单线程共享所有权 | Rc<RefCell<T>> |
Arc<T> | 多线程共享所有权 | Arc<Mutex<T>> |
RefCell<T> | 单线程运行时借用检查 | Rc<RefCell<T>> |
Mutex<T> | 互斥读写 | Arc<Mutex<T>> |
RwLock<T> | 多读者或单写者 | Arc<RwLock<T>> |
Weak<T> | 不延长生命周期的回指 | Rc::downgrade / Arc::downgrade |
Cell<T> | 单线程复制或替换内部值 | Cell<u32> |
001 · Box<T>:在堆上拥有值
用途: Box<T> 拥有堆上的 T。常用于让递归类型具有确定大小、持有 dyn Trait 特征对象,或转移较大值的所有权而不搬动值本身。
典型使用场景:
- 递归数据结构: 链表的下一个节点、语法树的子表达式、树节点的子节点等需要间接层,否则类型大小无法确定。例如
enum Expr { Add(Box<Expr>, Box<Expr>), Number(i32) }。 - 统一存放不同的具体类型: 一组实现同一特征的对象需要放入同一个集合,且每个对象由集合独占时,可使用
Vec<Box<dyn Trait>>。 - 为较大值提供固定大小的所有权句柄: 需要把值放进不同的容器或在函数之间转移所有权,又希望移动的是指针而非值本身时,可以考虑装箱;是否划算要看分配成本和实际值大小。
如果只是临时读取现有值,优先借用 &T;如果需要多个所有者,考虑 Rc<T> 或 Arc<T>。
用法: 使用 Box::new(value) 创建,使用 * 解引用。递归枚举中的 Box 为下一层提供固定大小的间接层。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum List {
Cons(i32, Box<List>),
Nil,
}
fn sum(list: &List) -> i32 {
match list {
List::Cons(value, next) => value + sum(next),
List::Nil => 0,
}
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
assert_eq!(sum(&list), 3);
}
常见操作:
| 调用 | 作用与用法 |
|---|---|
Box::new(value) | 把 value 放到堆上,返回拥有它的 Box<T>。 |
*boxed、&*boxed | 分别访问内部值、借用内部值;boxed 可变时还可通过 *boxed 修改。 |
Box::pin(value) | 创建 Pin<Box<T>>,用于需要固定值位置的场景,例如某些异步任务或自引用类型。 |
Box::leak(boxed) | 消耗 Box 并返回长期有效的 &mut T;此后不会自动释放该分配,适合确实要把值保留到程序结束的少数场景。 |
Box::into_raw(boxed) | 消耗 Box 并交出原始指针,主要用于 FFI;要避免泄漏,须在确保所有权唯一且指针有效的前提下用 unsafe { Box::from_raw(ptr) } 接回。 |
注意事项: Box<T> 只有一个所有者;移动 Box 会转移所有权,不会深拷贝 T。它不提供共享所有权或内部可变性。堆分配有成本,普通小值通常无需装箱。能否跨线程传递,仍取决于 T 的 Send / Sync 性质。
002 · Rc<T>:单线程共享所有权
用途: 让多个位置共同拥有同一个值,例如单线程的图节点。Rc 使用非原子的强引用计数;最后一个强引用销毁时,内部值被销毁。
典型使用场景:
- 单线程共享只读配置: 多个界面组件或处理器长期持有同一份配置,且它们的生命周期不方便由一个借用统一管理。
- 共享的数据结构节点: 多条路径指向同一个图节点,或持久化数据结构的新旧版本复用未改变的节点。
- 多个对象共同持有资源: 例如事件回调与界面状态共同持有一个对象,并希望最后一个持有者退出时自动释放。
仅仅需要短暂访问时,普通引用更简单;需要跨线程共享时改用 Arc<T>。如果共享节点需要修改内部状态,可组合 Rc<RefCell<T>>,并留意引用环。
用法: Rc::clone(&value) 创建指向同一分配的新所有者。
1
2
3
4
5
6
7
8
9
10
use std::rc::Rc;
fn main() {
let shared = Rc::new(String::from("Rust"));
let first = Rc::clone(&shared);
let second = Rc::clone(&shared);
assert_eq!(first.as_str(), "Rust");
assert_eq!(second.as_str(), "Rust");
assert_eq!(Rc::strong_count(&shared), 3);
}
常见操作:
| 调用 | 作用与用法 |
|---|---|
Rc::new(value) | 创建第一个强所有者。 |
Rc::clone(&shared) | 增加强引用计数,返回指向同一值的另一个 Rc,不会深拷贝 T。 |
Rc::strong_count(&shared) | 查看当前强引用数量,主要用于理解或诊断所有权;不要用它代替所有权设计。 |
Rc::downgrade(&shared) | 创建不延长内部值生命周期的 Weak<T>。 |
Rc::ptr_eq(&a, &b) | 判断两个 Rc 是否指向同一份分配,而不是比较内部值是否相等。 |
Rc::get_mut(&mut shared) / Rc::make_mut(&mut shared) | 前者只在没有其他强、弱引用时返回 Some(&mut T);后者在有其他强引用时克隆内部值(T: Clone),仅有弱引用时则与弱引用分离,提供写时复制。 |
注意事项: Rc::clone 只增加计数,不深拷贝 T。Rc<T> 不能跨线程共享,也不能仅凭共享的 Rc 取得 &mut T;单线程共享修改通常使用 Rc<RefCell<T>>。相互持有强 Rc 会形成引用环并泄漏内部值,回指应使用 Weak。
003 · Arc<T>:多线程共享所有权
用途: 与 Rc<T> 类似,但引用计数使用原子操作,适合跨线程共享所有权。
典型使用场景:
- 多个工作线程共享只读数据: 例如路由表、词典、模型参数或其他创建后基本不变的数据;每个线程持有一个
Arc。 - 后台任务共同持有服务状态: 多个任务的生命周期各不相同,但都需要保证同一份状态在任务结束前有效。
- 跨线程共享可变状态: 将
Arc与Mutex、RwLock或原子类型组合,分别负责所有权与并发访问控制。
如果只在线程内共享,Rc<T> 通常更合适;如果每个线程只需一份独立数据,直接移动或克隆数据可能比共享状态更简单。
用法: 每个线程持有一个 Arc::clone。只读共享时不需要锁。
1
2
3
4
5
6
7
8
9
10
11
12
use std::sync::Arc;
use std::thread;
fn main() {
let message = Arc::new(String::from("hello"));
let worker_message = Arc::clone(&message);
let worker = thread::spawn(move || {
assert_eq!(worker_message.as_str(), "hello");
});
worker.join().unwrap();
assert_eq!(message.as_str(), "hello");
}
常见操作:
| 调用 | 作用与用法 |
|---|---|
Arc::new(value) | 创建第一个可供线程共享的强所有者。 |
Arc::clone(&shared) | 增加强引用计数,常在把句柄移入新线程前调用。 |
Arc::downgrade(&shared) | 创建 std::sync::Weak<T>,用于跨线程场景中的非拥有关系。 |
Arc::strong_count(&shared) | 读取某一时刻的强引用数量;其他线程可能立即改变它,不能据此做同步决策。 |
Arc::ptr_eq(&a, &b) | 判断是否共享同一份分配。 |
Arc::get_mut(&mut shared) / Arc::make_mut(&mut shared) | 前者仅在没有其他强、弱引用时取得 &mut T;后者在有其他强引用时克隆内部值(T: Clone),仅有弱引用时则与弱引用分离。 |
注意事项: Arc 只保证引用计数的线程安全,不会自动让内部 T 线程安全;跨线程共享通常要求 T: Send + Sync。共享修改可使用 Arc<Mutex<T>>、Arc<RwLock<T>> 或原子类型。单线程优先考虑开销较低的 Rc<T>。强引用环也可能泄漏,可用 std::sync::Weak 打破。
004 · RefCell<T>:单线程运行时借用检查
用途: 当编译器无法从代码结构证明内部修改安全,而程序逻辑能保证借用规则时,允许通过共享引用修改内部值。常与 Rc<T> 组合。
典型使用场景:
- 单线程图或树的可变节点: 多个
Rc指向同一节点,需要在运行时更新其子节点、属性或缓存。 - 对象接口只有
&self,但需要更新内部状态: 例如记录调用次数、维护惰性计算结果或测试替身记录收到的调用;内部数据需要借用并原地修改。 - 借用关系取决于运行时流程: 程序能保证读写不会重叠,但编译器无法仅靠静态分析证明。
若修改的是简单 Copy 值,先考虑 Cell<T>;若可以通过 &mut self 完成修改,普通可变字段更直接。RefCell<T> 不替代多线程锁。
用法: borrow() 返回只读守卫 Ref<T>,borrow_mut() 返回可变守卫 RefMut<T>;守卫离开作用域后,借用结束。
1
2
3
4
5
6
7
8
9
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let shared = Rc::new(RefCell::new(vec![1]));
let other_owner = Rc::clone(&shared);
other_owner.borrow_mut().push(2);
assert_eq!(*shared.borrow(), vec![1, 2]);
}
常见操作:
| 调用 | 作用与用法 |
|---|---|
RefCell::new(value) | 创建在运行时检查借用规则的容器。 |
cell.borrow() / cell.borrow_mut() | 分别取得只读、可变守卫;借用冲突时会 panic。 |
cell.try_borrow() / cell.try_borrow_mut() | 尝试借用,冲突时返回 Err,适合需要自行处理失败的代码。 |
cell.replace(new_value) | 用新值替换并返回旧值;当前有活跃借用时会 panic。 |
cell.get_mut() | 已拥有 &mut RefCell<T> 时直接取得 &mut T,无需运行时借用检查。 |
cell.into_inner() | 消耗容器,取回内部的 T。 |
注意事项: 借用规则仍然存在,只是改在运行时检查:同一时刻只能有一个可变借用,或多个只读借用。冲突时 borrow() / borrow_mut() 会 panic;需要处理失败时使用 try_borrow() / try_borrow_mut()。尽量缩小守卫的作用域。RefCell<T> 不能用于跨线程共享修改。
005 · Mutex<T>:互斥访问
用途: 同一时刻只允许一个线程访问受保护的数据。跨线程共享时常用 Arc<Mutex<T>>:Arc 管所有权,Mutex 管同步。
典型使用场景:
- 多个线程更新同一份复合状态: 例如共享队列、统计汇总或若干必须一起更新的字段;整个更新过程需要保持一致。
- 读写都较频繁,或临界区很短: 不需要区分读锁和写锁时,
Mutex通常是清晰的起点。 - 一个线程拥有数据,但多个线程需要受控访问: 使用
Arc<Mutex<T>>为每个线程提供所有权句柄,并通过锁串行访问。
如果只是单个整数计数,可以考虑原子类型;如果读取远多于写入且读操作值得并行化,再考虑 RwLock<T>。若能通过消息传递让单个线程独占数据,也可避免共享锁。
用法: lock() 返回 LockResult<MutexGuard<T>>;守卫可解引用以读写数据,销毁时自动解锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut workers = Vec::new();
for _ in 0..4 {
let counter = Arc::clone(&counter);
workers.push(thread::spawn(move || {
let mut value = counter.lock().unwrap();
*value += 1;
}));
}
for worker in workers {
worker.join().unwrap();
}
assert_eq!(*counter.lock().unwrap(), 4);
}
常见操作:
| 调用 | 作用与用法 |
|---|---|
Mutex::new(value) | 创建保护 value 的互斥锁。 |
mutex.lock() | 阻塞直到取得锁,返回 LockResult<MutexGuard<T>>;守卫离开作用域后自动解锁。 |
mutex.try_lock() | 不等待锁,立即返回守卫或 WouldBlock / Poisoned 错误。 |
mutex.is_poisoned() | 查看锁目前是否中毒;并发线程可能随后改变状态,不能据此保证数据一定一致。 |
mutex.get_mut() | 已拥有 &mut Mutex<T> 时直接访问内部值,返回 LockResult<&mut T>,无需锁定。 |
mutex.into_inner() | 消耗锁并取出 T,仍需处理可能的中毒错误。 |
注意事项: lock() 可能阻塞;try_lock() 可非阻塞尝试。持锁线程 panic 时锁通常会中毒,lock() 返回 Err(PoisonError);应根据数据是否仍一致决定恢复或传播错误。缩短持锁时间,避免重复锁定同一个锁或以不一致顺序取得多个锁,以免死锁。普通 std::sync::Mutex 的守卫不应跨异步 .await 持有。
006 · RwLock<T>:多读者、单写者
用途: 允许多个读者同时读取,或一个写者独占访问。适合读多写少的共享数据;是否比 Mutex 快,应以实际负载测量。
典型使用场景:
- 运行时可更新的配置或缓存: 大量线程读取当前快照,少数操作偶尔更新内容。
- 读操作相对耗时: 同时允许多个读者可减少相互等待,但写者仍须等所有读者退出。
- 需要把读取和写入权限清楚地区分: 读取者取得读守卫,修改者取得写守卫,类型接口直接表达访问方式。
如果读操作很短、写入并不少,锁管理开销可能抵消并行读取的收益,此时先使用 Mutex<T> 并测量。完全不变的数据只需要 Arc<T>,不需要读写锁。
用法: read() 取得只读守卫,write() 取得可变守卫;常与 Arc 组合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let settings = Arc::new(RwLock::new(String::from("v1")));
let reader_settings = Arc::clone(&settings);
let reader = thread::spawn(move || {
let value = reader_settings.read().unwrap();
assert_eq!(value.as_str(), "v1");
});
reader.join().unwrap();
*settings.write().unwrap() = String::from("v2");
assert_eq!(settings.read().unwrap().as_str(), "v2");
}
常见操作:
| 调用 | 作用与用法 |
|---|---|
RwLock::new(value) | 创建读写锁。 |
lock.read() | 阻塞直到取得读守卫;可有多个读者同时持有。 |
lock.write() | 阻塞直到取得写守卫;写者独占访问。 |
lock.try_read() / lock.try_write() | 非阻塞尝试获取相应守卫;失败时处理占用或中毒错误。 |
lock.get_mut() | 已拥有 &mut RwLock<T> 时直接取得 LockResult<&mut T>,不必获取锁。 |
lock.into_inner() | 消耗锁并取出内部值;若已中毒,返回错误以便处理。 |
注意事项: 持有读锁时再请求写锁可能死锁,应先释放读守卫。read() / write() 可能阻塞,也有 try_read() / try_write()。写者持锁 panic 通常会使锁中毒;读者持锁 panic 通常不会。公平性和等待顺序依赖平台,不能假设写者一定优先。
007 · Weak<T>:不拥有值的弱引用
用途: 表示“可以找到对象,但不负责延长其生命周期”的关系。树中常让父节点强拥有子节点,子节点弱引用父节点,以免强引用环。Rc 对应 std::rc::Weak,Arc 对应 std::sync::Weak。
典型使用场景:
- 父子节点回指: 父节点强拥有子节点,子节点保存指向父节点的
Weak,便于向上查找而不形成强引用环。 - 观察者或缓存索引: 索引希望在对象仍存活时找到它,但不应仅因为索引存在就阻止对象释放;访问时尝试
upgrade()。 - 后台任务回看所属对象: 任务可以在对象仍存在时继续工作,对象已销毁时则通过
None结束相关操作。
当关系本身必须保证对象存活时应保留强 Rc / Arc;只有“允许对象先销毁”的关系才适合 Weak。
用法: 通过 Rc::downgrade(&strong) 或 Arc::downgrade(&strong) 创建;访问前调用 upgrade(),其结果为 Option<Rc<T>> 或 Option<Arc<T>>。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
name: String,
parent: RefCell<Weak<Node>>,
}
fn main() {
let parent = Rc::new(Node {
name: String::from("parent"),
parent: RefCell::new(Weak::new()),
});
let child = Rc::new(Node {
name: String::from("child"),
parent: RefCell::new(Rc::downgrade(&parent)),
});
assert_eq!(child.parent.borrow().upgrade().unwrap().name, "parent");
assert_eq!(child.name, "child");
drop(parent);
assert!(child.parent.borrow().upgrade().is_none());
}
常见操作: 以下以 std::rc::Weak<T> 为例;std::sync::Weak<T> 有对应的创建、升级和计数方法。
| 调用 | 作用与用法 |
|---|---|
Rc::downgrade(&strong) | 从强 Rc 创建弱引用;Arc 对应 Arc::downgrade(&strong)。 |
Weak::new() | 创建不指向任何分配的空弱引用,适合作为尚无父节点等状态的初始值。 |
weak.upgrade() | 尝试取得强 Rc<T>,返回 Option<Rc<T>>;若内部值已销毁则为 None。 |
weak.strong_count() | 查看当前仍有多少强引用;只能作观察,不能代替 upgrade() 判断可否访问。 |
weak.ptr_eq(&other) | 判断两个弱引用是否指向同一份分配;两个空的 Weak::new() 也被视为相等。 |
注意事项: 弱引用不增加强引用计数,不能保证内部值仍存在,每次 upgrade() 都应处理 None。内部值销毁后,只要还有 Weak,保存引用计数的控制块分配仍可能保留。设计图结构时,应明确哪些边拥有节点,哪些边只负责回指。
008 · Cell<T>:复制或替换内部值
用途: 在单线程中通过共享引用修改内部值,适合计数器、标志位等小型 Copy 值。它不像 RefCell 那样提供对内部值的普通借用。
典型使用场景:
- 带有
&self方法的轻量状态: 例如访问次数、是否已初始化、最近一次选中的编号;调用者不必持有整个对象的可变引用。 - 在回调或界面对象中切换状态: 读取旧值、计算新值并整体替换,且整个过程在单线程内进行。
- 非
Copy值的整体交换: 不需要借用内部字段,只需要用replace()取出旧值并放入新值时也可使用。
需要长期借用内部值或修改其某个字段时,改用 RefCell<T>;跨线程的简单标志位或计数器可考虑对应的原子类型。
用法: get() 在 T: Copy 时复制出值;set() 写入新值;replace() 替换并取出旧值。非 Copy 值也能使用 replace()、take()(要求 Default)或消耗自身的 into_inner()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::cell::Cell;
struct Stats {
visits: Cell<u32>,
}
impl Stats {
fn visit(&self) {
self.visits.set(self.visits.get() + 1);
}
}
fn main() {
let stats = Stats { visits: Cell::new(0) };
stats.visit();
stats.visit();
assert_eq!(stats.visits.get(), 2);
}
常见操作:
| 调用 | 作用与用法 |
|---|---|
Cell::new(value) | 创建内部值可被整体替换的容器。 |
cell.get() | 复制并返回当前值,要求 T: Copy。 |
cell.set(new_value) | 通过 &Cell<T> 写入新值,不要求 T: Copy。 |
cell.replace(new_value) | 放入新值并返回旧值,适合非 Copy 类型的整体交换。 |
cell.take() | 取出旧值,并留下 T::default();要求 T: Default。 |
cell.into_inner() | 消耗 Cell<T> 并取出内部值。 |
注意事项: Cell<T> 没有 borrow_mut(),不能直接取得内部值的 &T 或 &mut T;需要借用复杂结构并原地修改时使用 RefCell<T>。get() 要求 T: Copy,set() 不要求。Cell<T> 不能用于跨线程共享;简单的跨线程计数可考虑原子类型。
与智能指针有关的常用 Trait
Trait 描述类型能做什么。智能指针最常见的是 Deref、DerefMut 和 Drop;共享所有权和跨线程使用还涉及 Clone、Send、Sync。要注意包装类型与访问时返回的守卫类型不是同一个类型。
| 类型 | 与本文用法最相关的 Trait 或守卫 |
|---|---|
Box<T> | 实现 Deref<Target = T>、DerefMut;若 T: Clone,克隆 Box 会克隆内部值 |
Rc<T> / Arc<T> | 实现 Deref<Target = T> 和 Clone;克隆会增加强引用计数,不克隆内部值 |
Weak<T> | 实现 Clone,但不实现指向 T 的 Deref;必须先 upgrade() |
RefCell<T> | 容器本身不直接解引用到 T;borrow() 返回的 Ref<T> 实现 Deref,borrow_mut() 返回的 RefMut<T> 还实现 DerefMut |
Mutex<T> | 容器本身不直接解引用到 T;lock() 返回的 MutexGuard<T> 实现 Deref 和 DerefMut |
RwLock<T> | read() 返回的 RwLockReadGuard<T> 实现 Deref;write() 返回的 RwLockWriteGuard<T> 还实现 DerefMut |
Cell<T> | 不通过 Deref 暴露内部引用;使用 get()、set()、replace() 等方法操作值 |
Deref:像引用一样读取
Deref 的核心方法是 deref(&self) -> &Target。因此可以用 *pointer 读取内部值,也可以在需要 &Target 的地方利用解引用强制转换。Box<T>、Rc<T> 和 Arc<T> 都有此能力;Weak<T> 没有,因为目标值可能已经销毁。
1
2
3
4
5
6
7
8
9
fn print_length(text: &str) -> usize {
text.len()
}
fn main() {
let boxed = Box::new(String::from("Rust"));
assert_eq!(print_length(&boxed), 4); // &Box<String> → &String → &str
assert_eq!(boxed.len(), 4); // 自动解引用后调用 String 的方法
}
这里的自动转换只发生在引用等合适的位置;它不会把一个 Box<String> 的所有权自动转换成 String。编写自己的智能指针时,只有希望它自然表现得像目标类型、且解引用不会意外失败时,才实现 Deref。
DerefMut:通过可变守卫修改
DerefMut 在 Deref 基础上提供 deref_mut(&mut self) -> &mut Target。Box<T> 可直接使用;RefCell<T>、Mutex<T> 和 RwLock<T> 则需要先取得相应守卫,访问规则由容器负责检查。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::cell::RefCell;
use std::sync::Mutex;
fn main() {
let mut boxed = Box::new(1);
*boxed += 1;
let cell = RefCell::new(2);
*cell.borrow_mut() += 1; // RefMut<i32> 的 DerefMut
let mutex = Mutex::new(3);
*mutex.lock().unwrap() += 1; // MutexGuard<i32> 的 DerefMut
assert_eq!((*boxed, *cell.borrow(), *mutex.lock().unwrap()), (2, 3, 4));
}
Rc<T> 和 Arc<T> 不提供通常意义上的 DerefMut:共享所有权时不能无条件交出唯一的 &mut T。需要修改时使用前文的内部可变性或同步容器;如果确知没有其他强所有者,也可了解 Rc::get_mut / Arc::get_mut。
Drop:离开作用域时清理
Drop 定义值销毁时要执行的清理逻辑。Box 释放其拥有的堆分配;Rc、Arc 销毁一个句柄时减少强引用计数,最后一个强引用消失才销毁内部值;借用和锁的守卫离开作用域时解除借用或释放锁。通常无需手动为这些标准库类型实现 Drop。
1
2
3
4
5
6
7
8
9
10
11
12
13
use std::sync::Mutex;
fn main() {
let data = Mutex::new(vec![1]);
{
let mut guard = data.lock().unwrap();
guard.push(2);
} // guard 在此处被销毁,锁随即释放
let guard = data.lock().unwrap();
assert_eq!(*guard, vec![1, 2]);
drop(guard); // 需要提前释放时,调用 std::mem::drop
}
不能直接调用 Drop::drop(&mut value) 提前销毁某个值,应调用 drop(value) 转移其所有权。守卫的释放时机由其实际作用域决定;需要及时解锁时,可使用小作用域或显式 drop(guard)。
Clone:复制值还是增加所有者
Clone 的具体行为取决于类型,不能把所有 .clone() 都理解为深拷贝。Box<T>::clone() 在 T: Clone 时创建独立的内部值;Rc<T> 和 Arc<T> 的 clone() 增加强引用计数;Weak<T>::clone() 增加弱引用计数。共享指针建议写成 Rc::clone(&value) 或 Arc::clone(&value),使“增加所有者”的意图更明显。
1
2
3
4
5
6
7
8
9
10
11
12
use std::rc::Rc;
fn main() {
let boxed = Box::new(String::from("A"));
let boxed_copy = boxed.clone();
assert_ne!(boxed.as_ptr(), boxed_copy.as_ptr()); // 两份独立的字符串数据
let shared = Rc::new(String::from("B"));
let other_owner = Rc::clone(&shared);
assert!(Rc::ptr_eq(&shared, &other_owner)); // 指向同一份分配
assert_eq!(Rc::strong_count(&shared), 2);
}
Send 与 Sync:决定能否跨线程
这两个是标记 Trait:Send 表示值可以安全地转移到另一个线程;Sync 表示其共享引用 &T 可以安全地在线程间传递。它们没有要主动调用的方法,编译器会在创建线程等位置检查约束。
Rc<T>既不是Send也不是Sync;Arc<T>在T: Send + Sync时才能跨线程共享。Cell<T>、RefCell<T>不是Sync,因此Arc<RefCell<T>>不能变成可跨线程共享的可变容器。Mutex<T>在T: Send时可用于跨线程同步访问;RwLock<T>通常要求T: Send + Sync。这也是Arc<Mutex<T>>和Arc<RwLock<T>>常见的原因。- 不要为了绕过编译错误而随意手写
unsafe impl Send或unsafe impl Sync;那会把线程安全保证交给实现者自己。
选型顺序
- 只有一个所有者时,优先使用普通值或引用;需要堆上间接层时选
Box<T>。 - 需要共享所有权时,单线程选
Rc<T>;跨线程且T满足线程安全要求时选Arc<T>。 - 需要从共享引用修改数据时,单线程的小型值选
Cell<T>,动态借用选RefCell<T>;跨线程按访问模式选Mutex<T>、RwLock<T>或原子类型。 - 需要回指但不延长生命周期时,选对应
Rc或Arc的Weak<T>,并检查upgrade()的结果。