找回密码
立即注册
搜索
发回帖 发新帖

5285

积分

0

好友

671

主题
发表于 2 小时前 | 查看: 13| 回复: 0

本篇导读

上一篇的 Box<T> 是独占所有权的智能指针:一份数据只有一个 Box 拥有。但真实程序中,我们常常需要:

  • 一份数据被多个地方共同拥有(如图中被多个节点引用的节点、UI 中被多处共享的资源);
  • 在整体不可变的外表下,修改其内部某些数据(内部可变性)。

今天我们学习三个紧密相关的智能指针:


一、Rc<T>:让一份数据有多个所有者

Rc 是 Reference Counting(引用计数)的缩写。每当克隆一个 Rc,计数加一;每当一个 Rc 离开作用域,计数减一;计数归零时数据被释放。它是单线程场景下共享数据的方案(多线程用 Arc,第 18 篇会讲到)。

1. 为什么需要它:共享的 cons list

假设我们希望两个链表共享同一段公共尾部:

enum List {
    Cons(i32, Box<List>),
    Nil,
}
use List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a)); // a 被移动进 b
    // let c = Cons(4, Box::new(a)); // 错误:a 已经被移走
}

用 Rc 替换 Box:

use std::rc::Rc;

enum List {
    Cons(i32, Rc<List>),
    Nil,
}
use List::{Cons, Nil};

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("创建 a 后计数:{}", Rc::strong_count(&a)); // 1

    let b = Cons(3, Rc::clone(&a)); // 克隆 Rc,计数 +1(不复制底层数据)
    println!("创建 b 后计数:{}", Rc::strong_count(&a)); // 2
    {
        let c = Cons(4, Rc::clone(&a));
        println!("创建 c 后计数:{}", Rc::strong_count(&a)); // 3
    } // c 离开作用域,计数回到 2
    println!("内层结束后计数:{}", Rc::strong_count(&a)); // 2
}

2. Rc::clone 是廉价的

Rc::clone(&a) 只增加计数、复制一个指针,不会深拷贝底层数据,所以非常廉价。惯例用 Rc::clone 而非 a.clone(),便于在代码中识别"这是引用计数克隆"。

3. Rc 是不可变共享

通过 Rc 你只能获得不可变引用。这保证多所有者共享时不会有人修改数据。若需要可变,就要配合下面的 RefCell。


二、RefCell<T>:内部可变性

1. 内部可变性模式

正常情况下,借用规则在编译期强制:不可变就是不能改。内部可变性(interior mutability)是一种设计模式:一个类型对外提供不可变接口,但内部允许修改自身数据,把借用检查从编译期推迟到运行时。RefCell<T> 就是它的标准实现。

2. 什么时候用

  • 你能确信运行时借用规则不会被违反,但编译器无法在编译期证明(它只做局部、保守的分析);
  • 常见于:测试 mock 对象、需要可变缓存的不可变对象、某些设计模式。

3. 基本用法:borrow 与 borrow_mut

use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(5);

    // borrow_mut 获得可变借用
    *cell.borrow_mut() += 10;
    *cell.borrow_mut() += 2;

    // borrow 获得不可变借用
    println!("值:{}", cell.borrow()); // 17
}
  • borrow() 返回 Ref<T>,borrow_mut() 返回 RefMut<T>;
  • 它们在运行时执行借用规则:同一时刻多个 borrow,或一个 borrow_mut;
  • 如果运行时违反(比如同时持有两个 borrow_mut),程序会 panic,而不是允许数据竞争。

4. Rc + RefCell:可变的多所有者

二者组合极其强大:Rc 提供多个所有者,RefCell 提供可变性。

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let shared = Rc::new(RefCell::new(vec![1, 2, 3]));

    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);

    a.borrow_mut().push(4); // 通过 a 修改
    b.borrow_mut().push(5); // 通过 b 修改

    println!("{:?}", shared.borrow()); // [1, 2, 3, 4, 5]
}

5. RefCell 的代价

  • 借用检查发生在运行时,违反会 panic(而非编译报错);
  • 有极小的计数性能开销;
  • 只能用于单线程(Rc、RefCell 都不实现 Send)。多线程对应 Arc<Mutex<T>>。

三、引用循环与内存泄漏

Rust 几乎不可能发生普通内存泄漏,但 Rc + RefCell 组合可能制造引用循环:A 指向 B、B 又指向 A,两者计数永不为零,数据永不释放。

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Option<Rc<Node>>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(None),
        children: RefCell::new(vec![]),
    });

    let root = Rc::new(Node {
        value: 5,
        parent: RefCell::new(None),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    // 让 leaf 指回 root —— 如果用 Rc,就形成循环
    *leaf.parent.borrow_mut() = Some(Rc::clone(&root));
    // root -> leaf, leaf -> root:计数无法归零
}

四、Weak<T>:用弱引用打破循环

解决办法是把"回指"的一方改成弱引用 Weak<T>。Weak 不增加 strong_count,不拥有数据,也不阻止数据被释放;访问前要 upgrade 成 Option<Rc>。

把 Node 的 parent 字段改为 Weak:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>, // 弱引用指回父节点
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    let root = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    // Rc::downgrade 创建弱引用
    *leaf.parent.borrow_mut() = Rc::downgrade(&root);

    // upgrade 访问:数据还在返回 Some,已释放返回 None
    if let Some(parent) = leaf.parent.borrow().upgrade() {
        println!("leaf 的父节点值:{}", parent.value); // 5
    }
}

强弱引用的规则

  • 强引用计数 strong_count 归零才释放数据;
  • 弱引用计数 weak_count 不阻止释放;
  • 树结构惯例:父节点用 Rc 强拥有子节点,子节点用 Weak 弱指回父节点,这样从 root 释放时整棵树都能正确回收。

五、Cell:Copy 类型的轻量内部可变性

对于实现了 Copy 的简单类型(如 i32),还可以用更轻的 Cell<T>,通过 get/set 操作、无需引用:

use std::cell::Cell;

let c = Cell::new(1);
c.set(10);
println!("{}", c.get()); // 10
c.update(|x| x + 5);

Cell 不提供引用、只整体替换值,因此比 RefCell 更轻、不会 panic,但只适合 Copy 类型。


六、综合示例:可双向遍历的树

use std::cell::RefCell;
use std::rc::{Rc, Weak};

type NodeRef = Rc<RefCell<TreeNode>>;
type WeakNode = Weak<RefCell<TreeNode>>;

struct TreeNode {
    value: i32,
    parent: Option<WeakNode>,
    children: Vec<NodeRef>,
}

impl TreeNode {
    fn new(value: i32) -> NodeRef {
        Rc::new(RefCell::new(TreeNode {
            value,
            parent: None,
            children: Vec::new(),
        }))
    }

    fn add_child(parent: &NodeRef, child: &NodeRef) {
        parent.borrow_mut().children.push(Rc::clone(child));
        child.borrow_mut().parent = Some(Rc::downgrade(parent));
    }
}

fn main() {
    let root = TreeNode::new(1);
    let child1 = TreeNode::new(2);
    let child2 = TreeNode::new(3);

    TreeNode::add_child(&root, &child1);
    TreeNode::add_child(&root, &child2);
    TreeNode::add_child(&child1, &TreeNode::new(4));

    // 从 child1 向上找父节点
    let parent_value = child1
        .borrow()
        .parent
        .as_ref()
        .and_then(|w| w.upgrade())
        .map(|p| p.borrow().value);
    println!("child1 的父节点值:{parent_value:?}"); // Some(1)
}

这是树/图结构中 Rc、Weak、RefCell 组合的完整范式。


七、本篇小结

  1. Rc<T> 用引用计数实现单线程多所有者,Rc::clone 廉价、只增计数,只提供不可变共享;
  2. RefCell<T> 实现内部可变性,borrow/borrow_mut 在运行时检查借用规则,违反则 panic,仅单线程;
  3. Rc<RefCell<T>> 组合实现"多所有者 + 可修改";
  4. Rc 强引用互相指向会形成引用循环、造成内存泄漏;
  5. Weak<T> 弱引用不增 strong_count、不拥有数据,upgrade 后才能访问;树中父强子弱、子用 Weak 指回;
  6. Copy 小类型可用更轻的 Cell<T>。

八、动手练习

  1. 用 Rc 实现三个链表共享同一段尾部,打印各阶段 strong_count;
  2. 用 Rc<RefCell<i32>> 创建一个共享计数器,从两个不同所有者处递增;
  3. 故意制造两个 borrow_mut 同时活跃,观察运行时 panic 信息;
  4. 把树示例中 parent 改成 Rc(强引用),分析为何会泄漏,再改回 Weak;
  5. (选做)用 Rc/RefCell 实现一个简单的有向图,支持添加边,并从任意节点遍历。

下篇预告:第 18 篇|无畏并发

学完单线程的智能指针,下一篇我们进入多线程世界。Rust 凭借所有权、Send/Sync 两个标记 trait,在编译期保证"能编译就没有数据竞争"。届时将学习创建线程、消息传递(channel)、共享状态(Mutex + Arc),并理解 Send/Sync 的自动判定。

更多 Rust 编程学习资源与开发者交流,欢迎访问 云栈社区。




上一篇:Claude Mythos 5越界事件复盘:AI Agent安全评测为何拦不住供应链攻击
下一篇:HTTP2Shell 漏洞根源在 Brotli 解码器?波及 .NET、ClickHouse 等多组件
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-28 06:35 , Processed in 0.079813 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表