Box、Rc 与 Arc

本页目录

这些类型首先决定谁负责让值继续存活、谁最终释放它。Box 表达一个所有者;Rc 和 Arc 表达多个共享所有者。是否需要修改内部值、是否需要跨线程同步,是另一组问题。

类型所有权克隆的含义典型用途
Box独占T: Clone 时克隆内部值到新的 Box递归结构、拥有 DST、独立分配
Rc共享,非原子计数增加强引用,不克隆 T单线程共享结构
Arc共享,原子计数增加强引用,不克隆 T满足线程约束的共享所有权
Weak不保活 T增加弱引用反向链接、非拥有的观察者

Box 把递归尺寸变成间接引用

如果一个节点直接包含下一个同类型节点,其尺寸会递归依赖自身。通过 Box 间接持有下一个节点,当前节点的尺寸就可以确定。Box、Box<[T]> 还能拥有编译期不知道具体尺寸的值。

对于 Sized T,普通 Box 有单指针表示保证,但指针宽度依赖目标平台;DST 指针还携带相应元数据,不能统称为“8 字节”。零大小类型的 Box 不需要为 T 申请非零堆空间。Box

Box::new(large_array) 表达的是最终拥有堆上值,并不保证构造过程中绝无大数组栈临时量。大字节缓冲可从 Vec 构造再 into_boxed_slice,不要用源代码里只见一个指针来推断峰值栈用量。

fn main() {
    let bytes: Box<[u8]> = vec![0; 1_000_000].into_boxed_slice();
    assert_eq!(bytes.len(), 1_000_000);
    let moved = bytes; // 移动所有者,字节缓冲不因此整体搬迁
    assert_eq!(moved[0], 0);
}

Box 本身可以移动,不代表内部值被固定:通过合法的 &mut T 仍可替换或移出 T。需要地址固定契约时还涉及 Pin,见 内存布局。

移动 Box 与移动 Box 内的值

fn main() {
    let boxed = Box::new(String::from("owned"));
    let address = (&*boxed) as *const String;
    let moved = boxed;
    assert_eq!((&*moved) as *const String, address);
    let value: String = *moved;
    assert_eq!(value, "owned");
}

移动 Box 句柄时,示例中的堆上 String 对象仍在原处;之后 *moved 则取得 String 的所有权,消耗 Box。String 所拥有的字节缓冲与堆上 String 对象本身还要分开看。地址在一次句柄移动前后相同,并不证明 T 永远不可移动,更不能替代 Pin 的契约。

递归结构还应考虑销毁路径。Box 解决类型尺寸无限递归,不会自动消除长链逐层 Drop 的调用深度;非常长的链表可能需要显式迭代拆除。若只是连续元素,Vec 往往在布局和遍历上更直接。

Rc 和 Arc 共享的是同一个值

Rc::clone(&a) 与 Arc::clone(&a) 创建新的所有权句柄,通常不要求 T: Clone。值和计数如何排布属于实现细节,不应把某个平台上的字段字节数画成稳定 ABI。

Rc 既不 Send 也不 Sync。Arc 的计数可以跨线程协调,但对于默认分配器的 Arc,要实现 Send 和 Sync 仍要求 T: Send + Sync。Arc<RefCell> 不会自动得到线程安全的内部借用。Arc

Arc 的原子计数成本受架构、竞争和缓存影响,不能给出通用的“比 Rc 慢 10 倍”结论。频繁克隆是否重要,应结合实际热路径测量;借用 &T 足够时,也不必每次创建一个所有者。

值相等与同一个共享对象

use std::rc::Rc;
fn main() {
    let first = Rc::new(String::from("same"));
    let shared = Rc::clone(&first);
    let independent = Rc::new(String::from("same"));
    assert_eq!(first, independent); // 内容相等
    assert!(Rc::ptr_eq(&first, &shared));
    assert!(!Rc::ptr_eq(&first, &independent));
    assert_eq!(Rc::strong_count(&first), 2);
    drop(shared);
    assert_eq!(Rc::strong_count(&first), 1);
}

Rc::clone 明确表示复制所有权句柄;(*first).clone() 才调用 String 的 Clone。后者产生另一个 String,但一般类型的 Clone 也未必深拷贝全部可达数据,例如含 Arc 字段的结构可能仍共享该字段。因此“克隆了 T”不等于“得到完全隔离的对象图”。

当函数只在调用期间读取数据时,接收 &T 即可;只有需要保留共享所有权时,接收或克隆 Rc/Arc 才能表达相应契约。把所有参数都改成 Arc 会将分配与共享策略传播到并不需要它的接口。

Weak:不保活值,但能安全尝试获取所有权

至少一个强引用T 存活;Weak 可以 upgrade 获取强引用1最后一个强引用被释放析构 T;已有 Weak 的 upgrade 返回 None2最后一个 Weak 也被释放引用计数使用的剩余分配可以回收3值的生命期与引用计数分配的生命期普通 Rc/Arc 生命周期示意;Weak 不保活 T,也不等于裸指针。
use std::cell::Cell;
use std::rc::Rc;

struct Tracked(Rc<Cell<usize>>);
impl Drop for Tracked {
    fn drop(&mut self) { self.0.set(self.0.get() + 1); }
}

fn main() {
    let drops = Rc::new(Cell::new(0));
    let first = Rc::new(Tracked(Rc::clone(&drops)));
    let second = Rc::clone(&first);
    let observer = Rc::downgrade(&first);
    drop(first);
    assert!(observer.upgrade().is_some());
    assert_eq!(drops.get(), 0);
    drop(second);
    assert_eq!(drops.get(), 1);
    assert!(observer.upgrade().is_none());
}

upgrade 成功返回的新强引用,会在其存活期间保活 T;失败返回 None,而不是暴露一个可解引用的悬垂指针。最后一个强引用释放时 T 已析构,即使 Weak 仍让其背后的分配保留。

常见树模型是父节点强引用子节点,子节点用 Weak 指向父节点。关键不是固定哪个方向必须弱,而是所有权图中不能让不再需要的节点通过强引用环互相保活。仅有父到子的强引用不会构成环;双向都强引用才可能形成环。

反向链接要允许目标已经消失

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
    name: &'static str,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
    fn new(name: &'static str) -> Rc<Self> {
        Rc::new(Self {
            name, parent: RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }
}
fn main() {
    let child = Node::new("child");
    {
        let parent = Node::new("parent");
        *child.parent.borrow_mut() = Rc::downgrade(&parent);
        parent.children.borrow_mut().push(Rc::clone(&child));
        let owner = child.parent.borrow().upgrade().unwrap();
        assert_eq!(owner.name, "parent");
    }
    assert!(child.parent.borrow().upgrade().is_none());
    assert_eq!(child.name, "child");
}

child 有外部强所有者,因此 parent 销毁后仍可存活;其父链接却不能再升级。这是 Weak 允许的正常状态,调用者应处理 None。例子只展示一次连接;完整树 API 还需要维护旧父节点列表、禁止祖先成为后代等结构不变量,Weak 本身不会验证这些业务规则。

Weak 也不是自动清理的缓存。若 HashMap 的值都是 Weak,目标消失后键和弱句柄仍可能留在表里;需要在 upgrade 失败时删除或定期清扫。存活期与缓存容量是不同问题。

值销毁与分配释放是两个时刻

先创建 Weak,再释放所有强句柄:T 会先消失,控制分配仍保留给 Weak 使用。随后尝试 upgrade 会得到 None。最后释放 Weak,分配才可回收。反过来,在还有强句柄时 upgrade 成功,会真实增加一个需要管理的强拥有者。

正在呈现知识画面
值销毁与分配释放是两个时刻

最后一个强引用消失时 T 被析构;现存 Weak 保留控制分配,但不能复活 T。

修改共享值:先明确是否要让其他所有者看见

需求方式后果
已经唯一拥有,直接修改get_mut不克隆;存在其他强引用或 Weak 时返回 None
修改自己的版本make_mut多个强引用时克隆 T,旧所有者仍看见旧值
修改所有者共同看见的状态RefCell、Mutex 等内部可变性保持共享对象,另外约束访问
消费最后一个强所有者,取出 Ttry_unwrap强引用数为 1 即可,已有 Weak 不阻止成功

对于 Rc/Arc 的 make_mut,只有一个强引用但还有 Weak 时,会让这些 Weak 与新持有的值脱离关联,不需要克隆 T;Weak 后续不能升级到修改后的值。这与 get_mut 直接拒绝的行为不同。Rc::make_mut

use std::rc::Rc;

fn main() {
    let mut current = Rc::new(String::from("v1"));
    let snapshot = Rc::clone(&current);
    Rc::make_mut(&mut current).push_str("+edit");
    assert_eq!(&*snapshot, "v1");
    assert_eq!(&*current, "v1+edit");

    let weak = Rc::downgrade(&current);
    assert!(Rc::get_mut(&mut current).is_none());
    Rc::make_mut(&mut current).push('!');
    assert!(weak.upgrade().is_none());

    let another_weak = Rc::downgrade(&current);
    assert_eq!(Rc::try_unwrap(current).unwrap(), "v1+edit!");
    assert!(another_weak.upgrade().is_none());
}
只有一个强引用且没有 Weak存在多个强引用其他所有者保留旧版本一个强引用 + Weak弱观察者不保活 T直接得到 &mut T不克隆 T克隆 T,再修改新版本原句柄仍指向旧对象脱离已有 Weak不克隆 T;旧 Weak 失效make_mut:按当前拥有关系决定是否复制这是 make_mut 的行为分支;get_mut 不会替调用者复制或脱离 Weak。

写时克隆未必是深层快照

use std::cell::Cell;
use std::rc::Rc;
#[derive(Clone)]
struct Version {
    title: String,
    counter: Rc<Cell<u32>>,
}
fn main() {
    let mut current = Rc::new(Version {
        title: String::from("v1"), counter: Rc::new(Cell::new(0)),
    });
    let old = Rc::clone(&current);
    Rc::make_mut(&mut current).title.push_str(" edited");
    current.counter.set(7);
    assert_eq!(old.title, "v1");
    assert_eq!(current.title, "v1 edited");
    assert_eq!(old.counter.get(), 7);
    assert!(Rc::ptr_eq(&old.counter, &current.counter));
    assert!(!Rc::ptr_eq(&old, &current));
}

外层 Version 已分离,但派生 Clone 对 counter 只复制 Rc,两个版本仍看到同一个计数器。如果产品语义要求完整不可变快照,需要保证字段本身不可变,或明确克隆各层数据;仅在外面套一层 Rc 并调用 make_mut 不足以建立该保证。

取回 T 失败时,仍然持有原来的所有权

use std::rc::Rc;
fn main() {
    let first = Rc::new(String::from("keep me"));
    let second = Rc::clone(&first);
    let first = Rc::try_unwrap(first).expect_err("还有另一个强所有者");
    assert!(Rc::ptr_eq(&first, &second));
    drop(second);
    assert_eq!(Rc::try_unwrap(first).unwrap(), "keep me");
}

try_unwrap 失败返回 Err 中的原句柄,调用者可以继续保留它,而不是必须丢弃共享对象。Arc 的并发场景还要注意:多个线程分别执行 try_unwrap(...).ok() 可能都没取得 T,随后各自丢弃 Err,最终由析构释放 T。若希望所有强所有者都参与“恰有一个取走值”的协议,可研究 Arc::into_inner 的对应保证,并确保每个强句柄都遵循协议。Arc::into_inner

不要先检查 Arc::strong_count 再用 unsafe 制造 &mut T:计数查询与其他线程操作之间可能发生变化,且还涉及 Weak。使用 get_mut 等封装好的契约。内部可变性的具体选择见 Cell、RefCell 与 Mutex。