Rust 智能指针 II

你应该知道的常见智能指针

技术
技术Rust标准库编程语言特性智能指针

2026-03-30

了解过 DerefDrop 两个基本特征后,智能指针神秘面纱的一角已经显现出来。下面会介绍常见的智能指针。

Box<T>

Box<T> 只进行简单的堆内存分配,也就是将数据存放在堆上,同时在栈上保存一个指向堆的指针。它拥有其分配内存的所有权,并在离开作用域时释放其中内容。

let val: u8 = 5;
// 通过创建 `Box` 将值从栈移到堆
let boxed: Box<u8> = Box::new(val);
// 通过解引用将值从 `Box` 移回栈
let val: u8 = *boxed;

Box<T> 对于创建递归的数据结构很有用。如果不用 Box 包裹数据会无法经过编译检查,因为编译器无法推定整个数据结构的内存分配大小。

// 此枚举是递归的数据结构
// 如果第一个变体定义为 `Cons(T, List<T>)`
// 会无法通过编译
enum List<T> {
    Cons(T, Box<List<T>>),
    Nil,
}

let list: List<i32> = List::Cons(
    1, 
    Box::new(List::Cons(
        2, 
        Box::new(List::Nil)
    ))
);

由于 Box 是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗,代价是也没什么别的功能。

Box 和普通的数据在所有权上表现差不多:一个 Box 只能有一个所有者。

内部可变性

由所有权与借用规则可以推知:无法可变地借用一个不可变值。

let x = 5;
let y = &mut x;
// 编译失败,提示:
// cannot borrow `x` as mutable, as it is not declared as mutable

然而,特定情况下令一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的(这样值方法的外部代码就不能修改其值了)。为此我们提出内部可变性的概念。

内部可变性(Interior mutability)是 Rust 中的一种设计模式,它允许你即使在有不可变引用时也可以改变数据,简言之就是在不可变值内部改变值。这通常是借用规则所不允许的。

Cell<T>RefCell<T>OnceCell<T> 类型的值可以通过共享引用(即 &T 类型)进行修改,而大多数 Rust 类型只能通过可变引用(&mut T)进行修改。因此我们说这些容器类型提供了内部可变性(通过 &T 可变)。

要注意,Cell<T>RefCell<T>OnceCell<T> 类型都只适合单线程环境,它们均未实现 Sync。如果想在多线程环境中实现内部可变性,Mutex<T>RwLock<T>OnceLock<T> 或原子类型才是正确的选择。

Cell<T>

Cell<T> 通过将值移入和移出容器来实现内部可变性。通过把数据包装在 Cell 里,即使只有不可变引用(&Cell<T>)你也能修改内部的值。代价是你永远无法拿到内部值的引用。

换言之,一旦把数据放入 Cell,对其进行修改轻而易举(set 方法就行),但是想访问它却变得很困难,除非:

下面是使用 Cell 在不可变结构体内部实现可变性的例子:

use std::cell::Cell;

struct SomeStruct {
    regular_field: u8,
    special_field: Cell<u8>,
}

let my_struct = SomeStruct {
    regular_field: 0,
    special_field: Cell::new(1),
};

let new_value = 100;
my_struct.special_field.set(new_value);
// 如果写成
// my_struct.regular_field = new_value;
// 编译就会报错
// 因为 `my_struct` 是不可变的

// 但其内部的 `special_field` 字段是 `Cell`
// 因此在内部可变性的加持下
// 我们能修改 `special_field`
assert_eq!(my_struct.special_field.get(), new_value);

Cell<T> 通常用于复制或移动值时开销不大的简单类型(例如数字)。

RefCell<T>

Rust 的所有权与借用规则强有力地保障了程序运行时的内存安全。绝大多数情况下,编译器会在编译阶段就应用这些规则对代码进行分析检查:符合规则就放行;违反规则就抛出错误拒绝编译。这样既捕获了那些内存安全的隐患,又能让程序运行时的性能得到最大发挥。

但有些时候,这种编译时的行为会成为程序开发的阻碍。静态分析——正如 Rust 编译器所做的——天生保守,但代码的一些属性是不可能通过分析静态代码发现的。

很多情况下,你确信代码遵守了借用规则,而编译器却不能理解和确定。此时我们选择将对某些代码的借用检查从编译期延后至运行时。这相当于告诉编译器:“我为我的代码担保运行时不会出现内存安全问题,所以你也应该放我们编译通过。”

OnceCell<T> 某种程度上是 CellRefCell 的混合体

RefCell<T> 并不是绕开了借用规则,借用规则始终是生效的,只不过对规则的检查从编译期延后至运行时了。如果违反了这些规则,程序依然会在运行时崩溃而不是出现编译错误。

RefCell 的一种常见用法是与 Rc 结合使用。

Rc<T>

Rust 所有权机制要求一个值只能有一个所有者,在大多数情况下这都没有问题,但是考虑以下情况:

以上场景不是很常见,但是一旦遇到就非常棘手,为解决此类问题,Rust 在所有权机制之外又引入了额外的措施来简化相应的实现:通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者。

在单线程环境下,使用引用计数的智能指针是 Rc

Rc<T> 类型提供了对 T 类型值的共享所有权,该值在堆上分配。对 Rc 调用 clone 会生成一个新的指针,指向堆上的同一分配区域。当指向某分配区域的最后一个 Rc 指针被销毁时,该分配区域中存储的值(通常称为“内部值”)也会被丢弃。

实现内部可变性

在 Rust 中,共享引用默认禁止修改,Rc 也不例外:通常无法获得指向 Rc 内部值的可变引用。如果需要可变性,可以在 Rc 内部放置一个 CellRefCell

use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
use std::rc::Rc;

fn main() {
    let shared_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new()));
    // 创建一个代码块来限制借用的作用域
    {
        let mut map: RefMut<'_, _> = shared_map.borrow_mut();
        map.insert("africa", 92388);
        map.insert("kyoto", 11837);
        map.insert("piccadilly", 11826);
        map.insert("marbles", 38);
    }

    // Note that if we had not let the previous borrow of the cache fall out
    // of scope then the subsequent borrow would cause a dynamic thread panic.
    // This is the major hazard of using `RefCell`.
    let total: i32 = shared_map.borrow().values().sum();
    println!("{total}");
}

Rc 使用非原子引用计数。这意味着开销非常低,但 Rc 无法在线程间传递,因此 Rc 并未实现 Send trait。这样,Rust 编译器会在编译时检查以确保你不会在线程间传递 Rc。如果你需要多线程环境下的原子引用计数,请使用 sync::Arc

downgrade 方法可用于创建非所有权的 Weak 指针。Weak 指针可以升级为 Rc,但如果分配区域中存储的值已经被丢弃,则升级会返回 None。换句话说,Weak 指针不会保持分配区域内部值的存活状态,但它们会保持分配区域(内部值的后备存储)的存活。

Rc 指针之间的循环将永远不会被释放。因此,Weak 被用于打破循环。例如,一棵树中,父节点到子节点可以使用强 Rc 指针,而子节点回到父节点则可以使用 Weak 指针。

Rc<T> 会自动解引用为 T(通过 Deref trait),因此你可以在 Rc<T> 类型的值上直接调用 T 的方法。为了避免与 T 的方法名称冲突,Rc<T> 自身的方法都是关联函数,需要使用完全限定语法来调用:

use std::rc::Rc;

let my_rc = Rc::new(());
let my_weak = Rc::downgrade(&my_rc);

Rc<T> 对诸如 Clone 等 trait 的实现也可以使用完全限定语法来调用。有些人偏好使用完全限定语法,而另一些人则偏好使用方法调用语法。

use std::rc::Rc;

let rc = Rc::new(());
// 方法调用语法
let rc2 = rc.clone();
// 完全限定语法
let rc3 = Rc::clone(&rc);

Weak<T> 不会自动解引用为 T,因为内部值可能已经被丢弃。

克隆引用

通过为 Rc<T>Weak<T> 实现的 Clone trait,可以创建指向同一分配区域的新引用。

use std::rc::Rc;

let foo = Rc::new(vec![1.0, 2.0, 3.0]);
// 以下两种语法是等价的。
let a = foo.clone();
let b = Rc::clone(&foo);
// a 和 b 都指向与 foo 相同的内存位置。

Rc::clone(&from) 这种语法是最符合惯用写法的,因为它能更明确地表达代码的意图。在上面的例子中,这种语法能更清晰地表明代码是在创建一个新的引用,而不是复制 foo 的全部内容。

示例

考虑这样一个场景:一组 Gadget(小工具)归属于某个 Owner(所有者)。我们希望 Gadget 能够指向其 Owner。使用唯一所有权无法实现这一点,因为多个 Gadget 可能属于同一个 OwnerRc 允许我们在多个 Gadget 之间共享一个 Owner,并且只要还有 Gadget 指向它,Owner 就会保持分配状态。

use std::rc::Rc;

struct Owner {
    name: String,
    // ...其他字段
}

struct Gadget {
    id: i32,
    owner: Rc<Owner>,
    // ...其他字段
}

fn main() {
    // 创建一个引用计数的 `Owner`。
    let gadget_owner: Rc<Owner> = Rc::new(
        Owner {
            name: "Gadget Man".to_string(),
        }
    );

    // 创建属于 `gadget_owner` 的 `Gadget`。
    // 克隆 `Rc<Owner>` 会给我们一个新的指针,指向同一个 `Owner` 分配区域,
    // 同时增加引用计数。
    let gadget1 = Gadget {
        id: 1,
        owner: Rc::clone(&gadget_owner),
    };
    let gadget2 = Gadget {
        id: 2,
        owner: Rc::clone(&gadget_owner),
    };

    // 丢弃我们的局部变量 `gadget_owner`。
    drop(gadget_owner);

    // 尽管丢弃了 `gadget_owner`,我们仍然能够打印出 `Gadget` 所属 `Owner` 的名字。
    // 这是因为我们只丢弃了一个 `Rc<Owner>`,而不是它指向的 `Owner` 本身。
    // 只要还有其他 `Rc<Owner>` 指向同一个 `Owner` 分配区域,它就会继续保持存活。
    // 字段投影 `gadget1.owner.name` 之所以有效,是因为 `Rc<Owner>` 会自动解引用为 `Owner`。
    println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
    println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);

    // 在函数末尾,`gadget1` 和 `gadget2` 被销毁,随之销毁的是指向我们 `Owner` 的最后几个计数引用。
    // 此时,Gadget Man 也被销毁。
}

如果我们的需求发生变化,还需要能够从 Owner 遍历到 Gadget,我们就会遇到问题。从 OwnerGadgetRc 指针会引入一个循环。这意味着它们的引用计数永远不会变为 0,分配区域将永远不会被销毁:这就造成了内存泄漏。为了解决这个问题,我们可以使用 Weak 指针。

实际上,Rust 使得产生这种循环变得有些困难。要得到两个相互指向的值,其中一个必须是可变的。这很困难,因为 Rc 通过只提供对其包装值的共享引用来保证内存安全,而这些共享引用不允许直接修改。我们需要将希望修改的那部分值包裹在 RefCell 中,RefCell 提供了内部可变性:一种通过共享引用实现可变性的方法。RefCell 在运行时强制执行 Rust 的借用规则。

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

struct Owner {
    name: String,
    gadgets: RefCell<Vec<Weak<Gadget>>>,
    // ...其他字段
}

struct Gadget {
    id: i32,
    owner: Rc<Owner>,
    // ...其他字段
}

fn main() {
    // 创建一个引用计数的 `Owner`。注意,我们将 `Owner` 的 `Gadget` 向量放在了 `RefCell` 中,
    // 这样我们就可以通过共享引用对其进行修改。
    let gadget_owner: Rc<Owner> = Rc::new(
        Owner {
            name: "Gadget Man".to_string(),
            gadgets: RefCell::new(vec![]),
        }
    );

    // 像之前一样,创建属于 `gadget_owner` 的 `Gadget`。
    let gadget1 = Rc::new(
        Gadget {
            id: 1,
            owner: Rc::clone(&gadget_owner),
        }
    );
    let gadget2 = Rc::new(
        Gadget {
            id: 2,
            owner: Rc::clone(&gadget_owner),
        }
    );

    // 将 `Gadget` 添加到它们的 `Owner` 中。
    {
        let mut gadgets = gadget_owner.gadgets.borrow_mut();
        gadgets.push(Rc::downgrade(&gadget1));
        gadgets.push(Rc::downgrade(&gadget2));

        // `RefCell` 的动态借用在此结束。
    }

    // 遍历我们的 `Gadget`,打印其详细信息。
    for gadget_weak in gadget_owner.gadgets.borrow().iter() {

        // `gadget_weak` 是一个 `Weak<Gadget>`。由于 `Weak` 指针不能保证分配区域仍然存在,
        // 我们需要调用 `upgrade`,它会返回一个 `Option<Rc<Gadget>>`。
        //
        // 在这个例子中,我们知道分配区域仍然存在,所以我们直接对 `Option` 调用 `unwrap`。
        // 在更复杂的程序中,你可能需要对 `None` 的结果进行优雅的错误处理。

        let gadget = gadget_weak.upgrade().unwrap();
        println!("Gadget {} owned by {}", gadget.id, gadget.owner.name);
    }

    // 在函数末尾,`gadget_owner`、`gadget1` 和 `gadget2` 被销毁。
    // 此时不再有指向这些 gadget 的强 (`Rc`) 指针,因此它们被销毁。
    // 这也使得 Gadget Man 的引用计数归零,因此他也会被销毁。
}

参考资料