blog.dopana

Back

中级 Rust 系列的第 13 篇文章。在这篇文章中,普通的引用(&T)只是内存中的一个借用视图,而智能指针(Smart Pointers)是拥有所指向数据的一种数据结构。它们像引用一样工作,但带有额外的元数据和功能 — 堆分配、共享所有权和内部可变性。

graph TD
    A["智能指针"] --> B["Box<T><br/>堆分配<br/>单一所有者"]
    A --> C["Rc<T><br/>引用计数<br/>单线程共享"]
    A --> D["RefCell<T><br/>内部可变性<br/>运行时借用检查"]
    A --> E["Arc<T><br/>原子引用计数<br/>线程安全共享"]
    A --> F["Deref trait<br/>自定义 * 运算符"]

是什么让指针变得”智能”?#

普通的引用(&T)只是一个地址 — 它借用的是由他人拥有的数据。而智能指针则拥有它所指向的数据。当智能指针离开作用域时,数据会被自动丢弃,内存会被释放。

其中的关键是 Deref trait。它允许智能指针通过自定义 * 运算符的行为来表现得像普通引用。

use std::ops::Deref;

struct MyBox<T>(T);

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

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
rust

实现了 Deref 后,你可以使用 *my_box 就像使用 *reference 一样,Rust 会在后台自动调用 deref()

Box:将数据放在堆上#

Box<T> 是最简单的智能指针。它在堆上分配数据,并提供一个唯一的所有者。当 Box 被丢弃时,堆内存会被释放。

为什么使用 Box?#

  1. 递归类型:像链表或树节点这样的自包含类型需要间接引用来拥有已知的大小。
  2. 大数据传输:将大数据移动到堆上可以避免昂贵的栈复制。
  3. trait 对象:在一个公共接口后面存储不同类型的值。

使用 Box 的递归类型#

如果没有 Box,像链表这样的递归结构体将具有无限的大小:

graph LR
    A["Cons(1, Box)"] --> B["Cons(2, Box)"]
    B --> C["Cons(3, Box)"]
    C --> D["Nil"]

用于 trait 对象的 Box#

Box<dyn Trait> 允许在一个接口后面存储不同类型的值:

Rc:共享所有权(单线程)#

有时你需要多个所有者拥有相同的数据。Rc<T>(Reference Counted - 引用计数)在单线程场景中实现了这一点。

工作原理#

每次调用 Rc::clone(),引用计数就会增加。当一个克隆被丢弃时,计数就会减少。只有当计数达到 0 时,数据才会被释放。

use std::rc::Rc;

fn main() {
    let data = Rc::new(String::from("你好,共享的世界!"));
    println!("引用计数: {}", Rc::strong_count(&data)); // 1

    {
        let data2 = Rc::clone(&data); // 计数增加
        println!("引用计数: {}", Rc::strong_count(&data)); // 2
        println!("data2: {data2}");
    } // data2 被丢弃,计数减少

    println!("引用计数: {}", Rc::strong_count(&data)); // 1
    println!("data: {data}");
}
rust

使用 Rc 的树结构#

Rc<T> 非常适合子节点需要引用父节点的树结构:

[!NOTE] Rc<T> 使用非原子引用计数,速度更快但不是线程安全的。对于多线程代码,请使用 Arc<T>

RefCell:内部可变性#

RefCell<T> 启用了内部可变性 — 即使你只有一个对 RefCell 自身的不可变引用,也能修改其中的数据。

运行时的借用检查器#

Rc<T> 跟踪所有者的数量,而 RefCell<T> 在运行时跟踪引用的数量。规则与编译时借用相同,但违反规则会导致运行时 panic 而不是编译错误。

use std::cell::RefCell;

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

    {
        let mut borrow = cell.borrow_mut(); // 可变借用
        *borrow += 1;
        println!("在块内: {borrow}");
    } // borrow 在此处被丢弃

    println!("在块后: {}", cell.borrow()); // 不可变借用
}
rust

Rc + RefCell 的组合#

Rc<T>RefCell<T> 结合使用,可以获得共享所有权和修改能力:

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

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

    let data2 = Rc::clone(&data);
    data2.borrow_mut().push(4);

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

[!WARNING] RefCell<T> 在运行时会 panic,如果你违反借用规则(例如,同时进行两次可变借用)。这用灵活性交换了编译时安全性 — 请谨慎使用。

Arc:线程安全的共享所有权#

Arc<T>(Atomically Reference Counted - 原子引用计数)是 Rc<T> 的多线程版本。它使用原子操作来安全地在线程间共享数据。

graph TD
    A["Arc<Vec>"] --> B["线程 1<br/>Arc::clone"]
    A --> C["线程 2<br/>Arc::clone"]
    A --> D["线程 3<br/>Arc::clone"]
    A --> E["线程 4<br/>Arc::clone"]

[!TIP] Arc<T> 由于原子操作存在一定的性能开销。如果不需要线程安全性,Rc<T> 会更快。

何时使用哪种智能指针?#

智能指针使用场景线程安全?
Box<T>堆分配、递归类型、trait 对象✅ 是的(单一所有者)
Rc<T>多个所有者、单线程❌ 不是
RefCell<T>内部可变性、运行时借用检查❌ 不是
Arc<T>多个所有者、多线程✅ 是的
Rc<RefCell<T>>共享 + 可变、单线程❌ 不是
Arc<Mutex<T>>共享 + 可变、多线程✅ 是的

通俗比喻(ELI5):图书馆图书管理系统#

把智能指针想象成管理图书馆图书的不同方式:

  • Box<T> 就像借一本书 — 你是唯一的所有者。归还(drop)时,图书馆会收回图书。
  • Rc<T> 就像一本多个学习小组可以一起借阅的书。图书馆会跟踪借阅的小组数量。只有最后一个小组归还时,图书馆才会收回。
  • RefCell<T> 就像一本带有”只读”封面的书。你可以翻看它,但要在里面做笔记,你需要图书管理员(运行时检查器)的特别许可 — 而且一次只能有一个人写。
  • Arc<T> 就像 Rc<T> 但适用于大学城多校区共享同一本书的图书馆系统。原子计数器确保没有任何校区会丢失图书。

总结#

  • 智能指针拥有其数据并实现 Deref 来表现得像引用。
  • Box<T> 提供堆分配,是递归类型和 trait 对象的基础。
  • Rc<T> 通过引用计数在单线程代码中实现多所有者。
  • RefCell<T> 通过运行时借用检查提供内部可变性。
  • Arc<T> 是适用于多线程代码的 Rc<T> 线程安全版本。
  • 将这些组合在一起(例如 Rc<RefCell<T>>Arc<Mutex<T>>)为共享的、可变的状态管理提供了强大的模式。

在下一篇文章中,我们将探索Functional Rust — 闭包和迭代器 — 以及 Rust 的零成本抽象如何让你编写出富有表现力的高级代码,而不会牺牲性能。

参考资料#