Rust: Rust 智能指针:Box、Rc、RefCell 与内部可变性

最后更新:2026-08-26

智能指针(Smart Pointer)是"带额外行为的数据结构"——它像指针一样引用内存,但比普通指针多出自动内存管理、引用计数、借用检查等能力。

如果说普通指针是"一张写着地址的纸条",那智能指针就是"自带保安、清洁工和登记簿的物业管理系统"。Rust 的智能指针在编译时保证安全,没有运行时开销(除了引用计数的少量开销)。


1. 你将学到


2. 共享办公桌的故事

(1) 痛苦:多人想同时引用和修改同一份文档

周一早上,Luna 所在的公司推行"共享办公桌"政策。五个人的团队只有三张桌子。

"如果能知道有多少人还在看这份文档,等没人看了再销毁就好了……""如果能确保只有一个人在改的时候其他人不能改就好了……"

(2) Rust 智能指针的方案

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

fn main() {
    // Box: 一个人独占一份文档,看完就销毁
    let doc_box = Box::new(String::from("项目计划书 v1"));
    println!("Box 持有: {}", doc_box);
    // doc_box 离开作用域后自动销毁——不需要手动 free

    // Rc: 多人共享只读访问
    let doc_rc = Rc::new(String::from("共享计划书"));
    let alice = Rc::clone(&doc_rc);
    let bob = Rc::clone(&doc_rc);
    println!("引用计数: {}", Rc::strong_count(&doc_rc)); // 3

    // Rc<RefCell<T>>: 共享 + 可变
    let doc_shared = Rc::new(RefCell::new(String::from("协作文档")));
    let alice_view = Rc::clone(&doc_shared);
    let bob_view = Rc::clone(&doc_shared);

    // Bob 在文档上做批注(需要修改)
    bob_view.borrow_mut().push_str("\nBob 的批注:第二部分需要补充");
    // Alice 看文档(只读)
    println!("Alice 看到: {}", alice_view.borrow());
    // Carol 也看文档(只读)
    println!("Carol 看到: {}", doc_shared.borrow());
}

Rust 用三种智能指针解决了"共享"和"修改"的线程安全问题:Box 独占所有权和堆分配、Rc 共享只读所有权、RefCell 在运行时检查借用规则实现内部可变性。


3. 核心概念

(1) 智能指针体系

100%
graph TB
    A[Rust 智能指针] --> B[Box&lt;T&gt; 堆分配]
    A --> C[Rc&lt;T&gt; 引用计数]
    A --> D[RefCell&lt;T&gt; 内部可变性]

    B --> B1["let b = Box::new(42)"]
    B --> B2["离开作用域自动 drop"]

    C --> C1["Rc::clone 增加引用计数"]
    C --> C2["strong_count == 0 时销毁"]
    C --> C3["单线程环境使用"]

    D --> D1["borrow() → 不可变引用"]
    D --> D2["borrow_mut() → 可变引用"]
    D --> D3["运行时检查借用规则"]

    A --> E[组合模式]
    E --> E1["Rc&lt;RefCell&lt;T&gt;&gt;"]
    E --> E2["共享所有权 + 内部可变性"]

    A --> F[核心 Trait]
    F --> F1["Deref: 解引用运算符 *"]
    F --> F2["Drop: 析构函数"]

(2) 三种智能指针对比

特性 Box<T> Rc<T> RefCell<T>
所有权 单一所有权 共享所有权(引用计数) 单一所有权
可变性 可变(Box::new*b = val 不可变(只读共享) 内部可变(运行时检查)
借用检查时机 编译时 编译时 运行时
性能开销 无额外开销 引用计数增减(少量) 运行时借用检查(少量)
线程安全 否(单线程) 否(单线程)
适用场景 堆分配大数据、递归类型 多路径共享只读数据 需要在不可变引用下修改

(3) Deref 与 Drop trait

Trait 方法 作用
Deref fn deref(&self) -> &T *x 解引用操作可以作用于自定义类型
Drop fn drop(&mut self) 值离开作用域时自动调用的清理逻辑

(4) Box / Rc / Arc 选型对比

特性 Box<T> Rc<T> Arc<T>
所有权 单一所有者 多所有者(引用计数) 多所有者(原子引用计数)
线程安全 否(仅单线程) 否(仅单线程) 是(可跨线程共享)
引用计数 非原子,开销低 原子操作,开销略高
可变访问 &mut 直接可变 RefCell 配合 Mutex / RwLock 配合
典型场景 递归类型 / 大数据堆分配 DAG / 共享只读数据 多线程共享数据
性能 最快 中等 稍慢(原子操作)

Deref 让智能指针用起来像普通引用——Box<T> 可以像 &T 一样使用。Drop 保证了资源自动释放——不需要手动 freedelete


4. 智能指针示例

▶ 示例 1:Box——堆分配与递归类型(难度 ⭐)

RUST
// ============================================
// Box<T> 的三种典型用法:堆分配、递归类型、Deref
// ============================================

// 递归类型:Cons list(必须用 Box,因为编译时大小未知)
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

// 自定义智能指针(演示 Deref 和 Drop)
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0  // 返回内部值的引用
    }
}

impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        // 实际应用中在这里释放资源(如关闭文件、释放网络连接)
        // println!("MyBox 被销毁了~");
    }
}

fn main() {
    // --- 1. Box 基本堆分配 ---
    let b = Box::new(42);
    println!("Box 中的值: {}", b);  // 自动解引用

    // --- 2. 递归类型:Cons List ---
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("递归列表: {:?}", list);

    // --- 3. 自定义 MyBox + Deref ---
    let my_box = MyBox::new(String::from("Hello"));
    // Deref 让 &MyBox<String> 自动变成 &String,再自动变成 &str
    greet(&my_box);

    // --- 4. 解引用运算符 ---
    let x = 10;
    let y = MyBox::new(x);
    assert_eq!(10, *y);  // *y 等价于 *(y.deref())
    println!("*y == {}", *y);
}

fn greet(name: &str) {
    println!("Greeting: {}", name);
}

输出:

TEXT 📖 仅展示
Box 中的值: 42
递归列表: Cons(1, Cons(2, Cons(3, Nil)))
Greeting: Hello
*y == 10

Box<T> 的核心价值:一是把数据放到堆上(栈上只保留指针),二是让编译时大小未知的类型(如递归类型)可以正常使用。Deref trait 让 Box<T> 可以像 &T 一样被解引用——这就是"智能指针"的"智能"所在。


▶ 示例 2:Rc——引用计数共享所有权(难度 ⭐⭐)

RUST
// ============================================
// Rc<T> 引用计数:多路径共享只读数据
// ============================================

use std::rc::Rc;

#[derive(Debug)]
enum BookList {
    Cons(String, Rc<BookList>),
    Nil,
}

use BookList::{Cons, Nil};

fn main() {
    // --- 场景:三本书被两个读者共享 ---

    // 创建基础书单
    let book_c = Rc::new(Cons("Rust 编程".to_string(),
        Rc::new(Cons("算法导论".to_string(),
            Rc::new(Cons("设计模式".to_string(),
                Rc::new(Nil))))));

    println!("初始引用计数: {}", Rc::strong_count(&book_c));

    // Alice 借走了书单(Rc::clone 只增加引用计数,不深拷贝数据)
    let alice = Rc::clone(&book_c);
    println!("Alice 借走后引用计数: {}", Rc::strong_count(&book_c));

    {
        // Bob 也借走了书单(在内部作用域中)
        let bob = Rc::clone(&book_c);
        println!("Bob 借走后引用计数: {}", Rc::strong_count(&book_c));

        // 两个人都可以读
        println!("Alice 的书单: {:?}", *alice);
        println!("Bob 的书单: {:?}", *bob);
    }  // Bob 离开作用域,引用计数减少

    println!("Bob 还书后引用计数: {}", Rc::strong_count(&book_c));

    // Alice 还在读
    println!("Alice 还在读: {:?}", *alice);

    // 所有 Rc 都离开作用域后,数据才真正被销毁
    // Rc::strong_count == 0 时触发 drop
}

输出:

TEXT 📖 仅展示
初始引用计数: 1
Alice 借走后引用计数: 2
Bob 借走后引用计数: 3
Alice 的书单: Cons("Rust 编程", Cons("算法导论", Cons("设计模式", Nil)))
Bob 的书单: Cons("Rust 编程", Cons("算法导论", Cons("设计模式", Nil)))
Bob 还书后引用计数: 2
Alice 还在读: Cons("Rust 编程", Cons("算法导论", Cons("设计模式", Nil)))

Rc<T> 是"引用计数"(Reference Counted)的缩写。Rc::clone(&x) 并不深拷贝数据,只是把引用计数加 1。当所有 Rc 句柄都离开作用域(引用计数归零)时,数据才被销毁。Rc 只能用于单线程——多线程要用 Arc<T>


▶ 示例 3:RefCell——内部可变性(难度 ⭐⭐)

RUST
// ============================================
// RefCell<T>:运行时借用检查 + 内部可变性
// ============================================

use std::cell::RefCell;

// 模拟一个"记录日志"的 Messenger
// 外部只能拿到不可变引用,但内部需要修改日志
trait Messenger {
    fn send(&self, msg: &str);
}

// 日志记录器:内部使用 RefCell 存储消息
struct Logger {
    // 即使 Logger 本身不可变,messages 仍然可以修改
    messages: RefCell<Vec<String>>,
}

impl Logger {
    fn new() -> Logger {
        Logger {
            messages: RefCell::new(Vec::new()),
        }
    }
}

impl Messenger for Logger {
    fn send(&self, msg: &str) {
        // borrow_mut() 获取可变引用——即使 self 是 &self
        self.messages.borrow_mut().push(msg.to_string());
    }
}

fn main() {
    let logger = Logger::new();

    // 通过不可变引用调用 send——但内部实际上修改了
    logger.send("用户登录");
    logger.send("点击按钮");
    logger.send("数据提交成功");

    // borrow() 获取不可变引用
    let msgs = logger.messages.borrow();
    for (i, msg) in msgs.iter().enumerate() {
        println!("日志 #{}: {}", i + 1, msg);
    }

    // --- 运行时借用违反示例(取消注释会 panic)---
    // let mut_ref = logger.messages.borrow_mut();
    // let ref1 = logger.messages.borrow();    // panic! 同时有可变和不可变引用
    // println!("{}", ref1[0]);

    println!("RefCell 演示完成");
}

输出:

TEXT 📖 仅展示
日志 #1: 用户登录
日志 #2: 点击按钮
日志 #3: 数据提交成功
RefCell 演示完成

RefCell<T> 的核心是"内部可变性"(Interior Mutability):即使 Logger 本身是 &self 不可变引用,RefCell 仍然允许你修改内部数据。区别在于:编译时不检查借用规则,而是运行时检查——违反规则时程序会 panic,而不是编译报错。


▶ 示例 4:Rc<RefCell<T>>——共享 + 可变(难度 ⭐⭐⭐)

RUST
// ============================================
// Rc<RefCell<T>> 组合模式:共享所有权 + 内部可变性
// ============================================

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

// 模拟一个"共享白板"——团队成员可以写和读
#[derive(Debug)]
struct Whiteboard {
    content: RefCell<String>,
}

impl Whiteboard {
    fn new() -> Whiteboard {
        Whiteboard {
            content: RefCell::new(String::new()),
        }
    }

    fn write(&self, text: &str) {
        let mut content = self.content.borrow_mut();
        content.push_str(text);
        content.push('\n');
    }

    fn read(&self) -> String {
        self.content.borrow().clone()
    }
}

fn main() {
    // 创建一个共享白板,用 Rc 包裹使其可以被多人持有
    let board = Rc::new(Whiteboard::new());

    // Alice 和 Bob 都持有白板的引用
    let alice_board = Rc::clone(&board);
    let bob_board = Rc::clone(&board);
    let carol_board = Rc::clone(&board);

    println!("当前引用计数: {}", Rc::strong_count(&board));  // 4

    // Alice 在白板上写
    alice_board.write("Alice: 今天讨论 Rust 智能指针");
    // Bob 补充内容
    bob_board.write("Bob: 我来讲 Box 和 Rc 的区别");
    // Carol 也写
    carol_board.write("Carol: RefCell 的运行时借用检查很重要");

    // 所有人都能看到完整内容(因为共享同一个 RefCell)
    println!("=== 白板内容 ===");
    println!("{}", board.read());

    // 验证所有引用都指向同一份数据
    println!("Alice 看到的内容长度: {}", alice_board.read().len());
    println!("Bob 看到的内容长度: {}", bob_board.read().len());
    println!("Carol 看到的内容长度: {}", carol_board.read().len());

    // board 离开作用域时,引用计数归零,Whiteboard 自动销毁
}

输出:

TEXT 📖 仅展示
当前引用计数: 4
=== 白板内容 ===
Alice: 今天讨论 Rust 智能指针
Bob: 我来讲 Box 和 Rc 的区别
Carol: RefCell 的运行时借用检查很重要

Alice 看到的内容长度: 73
Bob 看到的内容长度: 73
Carol 看到的内容长度: 73

Rc<RefCell<T>> 是 Rust 单线程编程中最强大的组合模式之一:Rc 解决"多个人都想持有"的问题,RefCell 解决"持有不可变引用的人也想修改"的问题。如果把 Rc 比作"共享借阅卡",那 RefCell 就是"允许批注的特殊副本"。


▶ 示例 5:综合练习——图数据结构(难度 ⭐⭐⭐)

RUST
// ============================================
// 综合示例:Rc<RefCell<T>> 实现图节点
// ============================================

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

struct Node {
    value: String,
    neighbors: Vec<Weak<RefCell<Node>>>,
}

impl Node {
    fn new(value: &str) -> Rc<RefCell<Node>> {
        Rc::new(RefCell::new(Node {
            value: value.to_string(),
            neighbors: Vec::new(),
        }))
    }

    fn add_neighbor(node: &Rc<RefCell<Node>>, neighbor: &Rc<RefCell<Node>>) {
        node.borrow_mut().neighbors.push(Rc::downgrade(neighbor));
    }
}

impl fmt::Debug for Node {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let neighbor_names: Vec<String> = self.neighbors.iter()
            .filter_map(|w| w.upgrade())
            .map(|n| n.borrow().value.clone())
            .collect();
        write!(f, "{} -> {:?}", self.value, neighbor_names)
    }
}

fn main() {
    let a = Node::new("A");
    let b = Node::new("B");
    let c = Node::new("C");
    let d = Node::new("D");

    Node::add_neighbor(&a, &b);
    Node::add_neighbor(&a, &c);
    Node::add_neighbor(&b, &c);
    Node::add_neighbor(&b, &d);
    Node::add_neighbor(&c, &d);

    println!("=== 图结构 ===");
    for node in [&a, &b, &c, &d] {
        println!("{:?}", node.borrow());
    }

    println!("\n=== 从 A 出发可达的节点 ===");
    if let Some(neighbor) = a.borrow().neighbors.first().and_then(|w| w.upgrade()) {
        println!("A 的第一个邻居: {}", neighbor.borrow().value);
        let second_hop: Vec<String> = neighbor.borrow().neighbors.iter()
            .filter_map(|w| w.upgrade())
            .map(|n| n.borrow().value.clone())
            .collect();
        println!("从该邻居再走一步: {:?}", second_hop);
    }

    println!("\n=== 强引用计数 ===");
    println!("A 的 Rc 引用数: {}", Rc::strong_count(&a));
    let a_clone = Rc::clone(&a);
    println!("clone 后 A 的 Rc 引用数: {}", Rc::strong_count(&a));
    drop(a_clone);
    println!("drop 后 A 的 Rc 引用数: {}", Rc::strong_count(&a));
}

输出:

TEXT 📖 仅展示
=== 图结构 ===
A -> ["B", "C"]
B -> ["C", "D"]
C -> ["D"]
D -> []

=== 从 A 出发可达的节点 ===
A 的第一个邻居: B
从该邻居再走一步: ["C", "D"]

=== 强引用计数 ===
A 的 Rc 引用数: 1
clone 后 A 的 Rc 引用数: 2
drop 后 A 的 Rc 引用数: 1

图节点用 Rc<RefCell<Node>> 实现共享所有权 + 内部可变性;邻居关系用 Weak<RefCell<Node>> 避免循环引用导致内存泄漏。Rc::downgrade 创建弱引用,weak.upgrade() 尝试升级为强引用。


❓ 常见问题

Q Box<T> 和普通引用 &T 有什么区别?
A Box<T> 拥有数据的所有权,&T 只是借用。
Q Rc<T>Arc<T> 有什么区别?
A Rc<T> 是单线程引用计数,Arc<T> 是多线程原子引用计数。
Q RefCell<T>Cell<T> 有什么区别?
A Cell<T> 通过值拷贝(或移动)实现内部可变性,RefCell<T> 通过引用实现。
Q 什么时候用 RefCell<T> 而不是 &mut T
A 当你持有不可变引用但需要修改数据时用 RefCell
Q Rc<RefCell<T>>&mut T 在性能上有什么区别?
A Rc<RefCell<T>> 有引用计数和运行时借用检查的开销,&mut T 是零开销。

📖 小节


📝 作业

  1. 难度 ⭐:写一个程序,使用 Box<T> 创建一个递归的 Cons List(包含至少 3 个元素),然后打印整个列表。要求使用 #[derive(Debug)] 打印。
  2. 难度 ⭐⭐:实现一个"共享笔记"系统:使用 Rc<RefCell<String>> 让三个人(Alice、Bob、Carol)可以共享一个笔记字符串。每个人都能读,每个人都能写(追加内容)。验证所有人在写完后都能看到完整内容。
  3. 难度 ⭐⭐⭐:实现一个"图形观察者"模式。定义 trait Observer { fn notify(&self, msg: &str); },然后实现一个 LoggerObserver(内部用 RefCell<Vec<String>> 记录消息)。再用 Rc<RefCell<dyn Observer>> 让多个观察者可以被同一个 subject 通知。在 main 中创建两个观察者,subject 发送三条消息,验证两个观察者都收到了所有消息。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏