Rust 智能指针 III

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

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

2026-04-01

每种智能指针都有其特定的适用场景,区分不同智能指针的优劣对实际应用是很有好处的。也许下面介绍的几种并不那么通用,但它们仍然在自己擅长的领域发光发热。

Pin<Ptr>

什么是 move

当我们在其他编程语言的语境下说一个值被移动(move),一般都在说物理内存层面发生的事件——内存中的某个值被编译器逐个字节地复制到了另一处区域,也就是这个值所在的内存地址发生了变动。

如果单从物理层面来看,这与 Copy 特征所做的差不多——都是在内存中复制值。

但事实上,Rust 当中的“移动”是说:所有权从一个变量转移到另一个变量。也就是说 Rust 所说的“移动”是关于所有权的概念,而其他编程语言的移动大多是在谈论物理内存层面的事情。

let a = String::from("hello");
let b = a;
// 这个 `String` 值的所有权从 a 转移到 b
// 因此在这里 a 便不再可用

因此我们区分 Copy 和“移动”的主要依据是所有权有没有发生转移——Copy只是复制了值,但所有权依然归属于原先被拷贝的变量;而“移动”意味着所有权在变量间转移。

let a = 5i32;
let b = a;
// b 只复制了 a 的值,并没有取走所有权
// 所有权没有转移,两个变量均有效

后面所有斜体形式的移动,都是指物理内存层面所发生的地址变动;而带引号形式的“移动”,则是指 Rust 所有权意义上的转移。


Rust 中的所有值本质上都是可移动的。这意味着值所在的内存地址在不同的借用之间不一定是稳定不变的。

编译器被允许将一个值移动到一个新地址,而无需运行任何代码来通知该值它的地址已经改变。虽然编译器不会在没有发生所有权“移动”的地方进行内存层面的移动,但在其它地方(比如赋值或将值传递给函数时)就有机会发生移动。看下面这个例子:

/// 一个能追踪自身所处内存地址的结构体类型
#[derive(Default)]
struct AddrTracker(Option<usize>);

impl AddrTracker {
    fn check_for_move(&mut self) {
        // 将结构体强转为裸指针,然后再转为 `usize` 值
        let current_addr = self as *mut Self as usize;
        match self.0 {
            // 如果结构体内部没有值,就填入结构体的地址值
            None => self.0 = Some(current_addr),
            // 如果结构体内部已有地址值,检查已有地址与当前地址值是否相同
            // 若不相同则直接 panic
            Some(prev_addr) => assert_eq!(prev_addr, current_addr),
        }
    }
}

// 创建一个实例并在其内部存储其自身的初始地址
let mut tracker = AddrTracker::default();
tracker.check_for_move();

// 这里我们通过变量遮蔽机制
// 主动触发所有权*移动*,这样
// 可能伴随着内存层面的“移动”
let mut tracker = tracker;

// 执行下面这条语句
// 你会发现结果几乎总是 panic
tracker.check_for_move();

如果一种类型的值能被随意移动,而它本身并不在乎这一点,那么它就是 Unpin 的——编译器自动为其实现了 Unpin 特征。

`Unpin` 特征

pub auto trait Unpin { }

Unpin 是一个标记特征,它表示类型不需要任何固定保证,换言之,表示类型可以在内存中安全地移动

绝大多数类型都不在意是否被移动,所以几乎你能想到的每个类型都会自动实现此特征——只要构成此类型的所有字段类型也是 Unpin 的。

但 Rust 保留了一个例外——std::marker::PhantomPinned。如果一个类型包含了 PhantomPinned,那么编译器不会为之自动实现 Unpin

后面会讲:可以被 Pin 住的值实现的特征是 !Unpin(即没有实现 Unpin 的意思)。实现了 Unpin 的类型虽然仍可以被放入 Pin,但固定作用实质上已经失效了。

移动相对的概念是固定。当某个值被置于一种状态,保证从它进入该状态起直至调用其 drop 时,它都会保持位于内存的同一个地方,我们就说这个值已经被固定(pinned)了。

这种固定状态在构建某些类型的安全接口时是很有必要的,因为类型在某些状态下会变得对地址敏感(address-sensitive),而处于这种地址敏感状态的值不能随意被移动。典型的地址敏感类型是自引用类型。

自引用(self-referential)类型
该类型的某个字段是另一个字段的引用形式。自引用类型广泛存在于异步 Rust 中。
该类型的值被移动时,它的所有字段会一起被移动,此时引用字段的值会变为悬垂引用。因此这种类型的值不能随意被移动

为了从源头上切断值被移动的可能,我们不能获取值的可变引用。因为一旦你拿到了可变引用,就可以对这个值做手脚(比如使用像 mem::swapmem::replace 这样的函数),而这往往会导致值被移动。因此需要采取措施来避免对外暴露出可变引用。

常见的智能指针类型(如 Box<T>)允许移动它们指向的底层值:你可以从 Box<T> 中移出值,或使用 mem::replace&mut T 中移出 T。因此仅仅将一个值放入智能指针不足以确保其地址不会改变。我们还需要想别的办法。

庆幸的是,Pin 就是这个问题的最优解。它的整体思路就是如果你不能被移动,那把你放到我这里,我保证不会在 Safe Rust 中暴露出任何获取可变引用的接口。如果是 unsafe Rust,那就由程序员自己保证不会进行可能造成移动的操作。

Pin

为了固定一个值,我们将一个指向该值的指针(类型为某种 Ptr)包装进 Pin<Ptr> 中。Pin<Ptr> 可以包装任何指针类型,并承诺被指向对象(pointee)在销毁前不会被移动或失效。

Important

Pin 包装的并非我们要固定的值本身,而是指向那个值的指针!Pin<Ptr> 并不会固定 Ptr;相反,它固定的是 Ptr 指向的值。

任何被放入 Pin 的指针类型 P<T> 都需要与 Pin 订立以下契约:

用人话来解释此两条契约,可以这么说:

此处隐含了一个问题:!UnpinP<T> 虽然在 Pin 当中有安全保障,但那也是已经放入 Pin 时的事了。还没放入 Pin 的时候谁来保障安全?

答案出乎意料地简单粗暴:程序员。

如何创建和使用 Pin

Pin 会告诉程序员说,你的 !Unpin 数据放我这儿绝对放心,但是你得给我小心翼翼地送过来。在这个过程中出现任何意外,编译器可不帮你买单!

所以如果你检索文档中创建 Pin 的方法,会发现有两个:

impl<Ptr: Deref<Target: Unpin>> Pin<Ptr>
    pub const fn new(pointer: Ptr) -> Pin<Ptr>

impl<Ptr: Deref> Pin<Ptr>
    pub const unsafe fn new_unchecked(pointer: Ptr) -> Pin<Ptr>

根据方法签名,new 只适用于底层数据是 Unpin 的情况,并且这个方法是安全的。想想也不意外,既然数据能被安全移动,那就不用管那么多有的没的,直接送过来就行。

new_unchecked 对底层数据没什么限制——言外之意是说 !Unpin 的数据要从这里走——但是这个方法是 unsafe 的。之所以这么设计,就是因为编译器不敢保证程序员能老老实实地不进行那些可能造成移动的操作,所以编译器也摆烂,转而让程序员不得不自己保证数据的内存安全。但是一个人人皆知的事实是——没有人是完全靠得住的,程序员也一样。所以这里 unchecked 就表示编译器不代为检查内存安全了,而 unsafe 标示着程序员需要手动维护内存安全,一旦出事,编译器不为程序员的行为买单。


除了创建以外,其它很多方法都有一个 safe 的版本和对应的 unsafe 版本,并且这个 unsafe 版本的方法名也总是带有 unchecked 字样。例如:

impl<Ptr: Deref<Target: Unpin>> Pin<Ptr>
    pub const fn into_inner(pin: Pin<Ptr>) -> Ptr

impl<'a, T: ?Sized> Pin<&'a mut T>
    pub const fn get_mut(self) -> &'a mut T
    where
        T: Unpin,

// 对应 `unsafe` 版本:

impl<Ptr: Deref> Pin<Ptr>
    pub const unsafe fn into_inner_unchecked(pin: Pin<Ptr>) -> Ptr

impl<'a, T: ?Sized> Pin<&'a mut T>
    pub const unsafe fn get_unchecked_mut(self) -> &'a mut T

原理和上面是一样的。如果被指向值的类型实现了 Unpin,那么就直接大胆用,不管怎么折腾也不会有内存安全问题;如果该值的类型没有实现 Unpin,那么 Rust 通过 unsafe 告诉程序员让他自己负责内存安全,保证这种地址敏感的值不会发生移动

还有一些常用方法可能第一眼会让你困惑,这里也进行简要介绍:

/// 内部指针实现可变解引用时的方法
impl<Ptr: DerefMut> Pin<Ptr>
    /// 相当于去掉指针,直接把底层数据的可变引用放在 `Pin` 当中
    /// 比如把 `Pin<Box<T>>` 转换成 `Pin<&mut T>`
    /// 
    /// 这个方法在多次调用消耗内部指针的函数时是很有用的
    /// 
    /// 这个方法也是安全的,因为它并没有把数据的可变引用直接暴露给程序员
    /// 而只是做了一次转递,内部 `!Unpin` 数据依旧在 `Pin` 的保障之下
    pub fn as_mut(&mut self) -> Pin<&mut Ptr::Target>
    where
        Ptr: DerefMut,
    
    /// 修改底层数据的值
    /// 
    /// 这个方法是安全的,因为它会在内存的当前位置原地覆盖旧值
    /// 不会触发*移动*
    pub fn set(&mut self, value: Ptr::Target)
    where
        Ptr::Target: Sized,

core::pin 还提供一个 pin 宏,它可以从一个 T 值快速创建出 Pin<&mut T>

use core::pin::{pin, Pin};

fn stuff(foo: Pin<&mut Foo>) {
    // …
}

let pinned_foo = pin!(Foo { /* … */ });
stuff(pinned_foo);
// 或者直接这样写:
// stuff(pin!(Foo { /* … */ }));

两个使用 Pin 的例子

下面的例子演示了自引用结构体的创建以及如何使用 Pin 保障其安全。

use std::pin::Pin;
// `PhantomPinned` 是一种标记类型
// 当某一类型包含它时,该类型不会自动实现 `Unpin`
use std::marker::PhantomPinned;
// 非空的裸指针类型
use std::ptr::NonNull;

/// 这是一个自引用结构体,它的 `slice` 字段将指向 `data` 字段
struct Unmovable {
    /// 后备缓冲区字段
    data: [u8; 64],
    /// 指向 data 字段的非空裸指针
    /// 因为所有权与借用系统不允许这里是引用
    /// 所以只能采用裸指针
    slice: NonNull<[u8]>,
    /// 防止类型自动实现 `Unpin`
    _pin: PhantomPinned,
}

impl Unmovable {
    /// 为了保证数据不会*移动*,我们先将之放入分配堆内存的 `Box` 中
    /// 要注意虽然数据被固定了,但 `Pin<Box<Self>>` 本身仍是可*移动*的
    /// 这对我们来说很重要,因为这样就允许我们将它从函数中返回
    /// 而这本身也是一种*移动*行为
    fn new() -> Pin<Box<Self>> {
        let res = Unmovable {
            data: [0; 64],
            // 只有等所有都就位了,我们才创建指针
            // 否则在真正的演示开始前它可能就已经被*移动*过了
            slice: NonNull::from(&[]),
            _pin: PhantomPinned,
        };
        // 将数据放进 Box 中
        let mut boxed = Box::new(res);

        // 让 `slice` 字段指向这个被 `Box` 包裹的数据的 `data` 字段
        // 从现在起,我们需要确保这部分数据不发生*移动*
        boxed.slice = NonNull::from(&boxed.data);

        // `into_pin` 会将 `Box<T>` 转换为 `Pin<Box<T>>`,且
        // 如果 `T` 未实现 `Unpin`,那么 `*boxed` 就会被固定住
        let pin = Box::into_pin(boxed);

        // 返回已经固定住底层数据的 `Pin`
        pin
    }
}

let unmovable: Pin<Box<Unmovable>> = Unmovable::new();

// 内部的 `Unmovable` 结构体无法被*移动*
// 但是对外部的指针来说,移动依旧是自由的
let mut still_unmoved = unmovable;
assert_eq!(still_unmoved.slice, NonNull::from(&still_unmoved.data));

还记得最开始的 AddrTracker 吗?我们使用 Pin 来修复它:

use std::marker::PhantomPinned;
use std::pin::Pin;
use std::pin::pin;

#[derive(Default)]
struct AddrTracker {
    prev_addr: Option<usize>,
    // `PhantomPinned` 防止自动实现 `Unpin`
    _pin: PhantomPinned,
}

impl AddrTracker {
    fn check_for_move(self: Pin<&mut Self>) {
        let current_addr = &*self as *const Self as usize;
        match self.prev_addr {
            None => {
                // 还没有之前的地址数据
                // 就用 `get_unchecked_mut` 获取暴露的底层数据的可变引用
                let self_data_mut = unsafe { self.get_unchecked_mut() };
                // 执行完下面这条语句,某种意义上底层数据就变得地址敏感了
                self_data_mut.prev_addr = Some(current_addr);
            },
            // 检查已有地址与当前地址值是否相同
            Some(prev_addr) => assert_eq!(prev_addr, current_addr),
        }
    }
}

// 创建该值,此时还未处于地址敏感状态
let tracker = AddrTracker::default();

// 通过 `pin` 宏将该值放入 `Pin`
let mut ptr_to_pinned_tracker: Pin<&mut AddrTracker> = pin!(tracker);
// 可以看出 `as_mut` 经常用在需要多次调用消耗所有权的函数的场景
ptr_to_pinned_tracker.as_mut().check_for_move();

// 下面这个调用绝对不会引发 panic 了
ptr_to_pinned_tracker.as_mut().check_for_move();

Pin 还能用来做什么

在 Rust 社区,流传着这样一句名言:

Whenever you wonder if Pin could be the solution, it isn't.

事实也确实如此,绝大多数情况下,我们的问题都没到需要使用 Pin 的地步。但确实有一些场景需要用到它。

侵入式集合(intrusive collection)
一种特殊的数据结构设计模式。在侵入式集合中,被存储的元素自身包含了集合所需的管理信息(如指针、节点属性等),而不是由集合去动态分配一个独立的“节点”来包装这些元素。
与之相反的是非侵入式集合(non-intrusive collection)。我们常用的几乎所有集合类型都是非侵入式的,比如 VecLinkedList。非侵入式集合中的元素和集合自身是完全解耦的,集合不需要关心每一个元素的类型是什么,集合也可以用来存放任意类型的元素。

例子用 C++ 说明了侵入式集合与非侵入式集合之间的区别:

/* 非侵入式集合 */

// 集合内部的元素
struct Data {
    int value;
};
// 集合内部的节点(对用户通常不可见)
struct Node {
    Data data;
    Node* next;
    Node* prev;
};

/* 侵入式集合 */

// 数据结构本身就包含了链表需要的指针
struct IntrusiveData {
    int value;
    IntrusiveData* next;
    IntrusiveData* prev;
};
// 集合只维护头尾指针,不负责分配节点
class IntrusiveList {
    IntrusiveData* head;
    // ...
};

在 Rust 当中编写侵入式集合需要使用 Pin 来进行固定。原因在于元素之间互相有 prevnext 指针指向自己,如果中间某个元素发生了移动,那其他元素指向它的指针地址就失效了,导致不安全行为。

Cow<'a, B>

ToOwnedBorrow 特征

当只有一个不可变引用,却想对其进行修改或获取所有权时,大家很可能会使用 Clone 特征来从借用数据克隆出拥有所有权的数据。但是某种意义上 Clone 是狭隘的:它只提供从 &T 转换为 T 的功能。而 ToOwnedClone 进行了泛化。即,它可以将 &T 转换为 U 类型。下面是 ToOwned 的定义:

pub trait ToOwned {
    type Owned: Borrow<Self>;

    // Required method
    fn to_owned(&self) -> Self::Owned;

    // Provided method
    fn clone_into(&self, target: &mut Self::Owned) { ... }
}

根据定义,T 若实现了 ToOwned,那么 &T 能通过调用 to_owned 转换为 Owned 类型。这个 Owned 是一个实现了 Borrow<Self> 特征的类型。Borrow 又是什么玩意?

`Borrow<Borrowed>` 特征

经常能见到标准库中一些类型为另一些类型提供了额外的功能扩展,而这些功能可能会带来性能开销。就比如 String 类型在原生类型 str 的基础上增加了一些字符串的可扩展能力,代价就是需要维护一些对于简单不可变字符串来说并不必要的额外信息。

这些提供扩展功能的类型通常会通过返回对底层数据类型的引用来提供访问能力。而 Borrow 特征就是干这件事的,它声明了某个类型可以返回的底层数据是何种类型,并提供了访问方法。

Borrow 特征定义如下:

pub trait Borrow<Borrowed>
where
    Borrowed: ?Sized,
{
    // 需要实现的方法
    fn borrow(&self) -> &Borrowed;
}
// 一个类型通过实现 `Borrow<T>` 来表达它们可以被借用为类型 `T`
// 并通过该特征的 `borrow` 方法返回 `&T`

在官方文档中,你可以看到很多老面孔:

  • String 实现了 Borrow<str>

  • Box<T>Rc<T>Arc<T> 都实现了 Borrow<T>

如果一个类型还希望支持以可变方式借用,从而允许修改底层数据的话,那么它还可以额外实现 BorrowMut<T>

综上所述,T 若想实现 ToOwned,需要一个实现了 Borrow<T> 的关联类型,这意味着有个类型能被借用为 T。而 T 实现 ToOwned 又表明 T 能被转为一个持有所有权的类型。

所以这两个特征的作用实际是相互的。

让我们看看现实是不是这样:

str 类型实现了 ToOwned,且关联类型 OwnedString,而上面提过 String 实现了 Borrow<str>,因此我们可以对 &str 调用 to_owned 来转为一个 String

let slice = "Hello world";
assert_eq!(slice.to_owned(), slice.to_string());

还真是!

看完了 ToOwnedBorrow 特征,下面可以进入正题了。

写时克隆的 Cow

Cow<'a, B> 是一种提供写时克隆(Clone-on-Write)功能的智能指针。它可以封装借用数据并提供对其的不可变访问,同时在需要修改或获得所有权时按需惰性地克隆数据。

pub enum Cow<'a, B>
where
    B: ToOwned + ?Sized + 'a,
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

从定义不难看出,Cow 本质是一个枚举,两个变体分别表示它内部的数据是引用状态还是自有状态。如果初始化传入的是引用,那么变体就是 Borrowed;如果再进行修改或尝试获取所有权,那么变体会成为 Owned

use std::borrow::Cow;

fn abs_all(input: &mut Cow<'_, [i32]>) {
    for i in 0..input.len() {
        let v = input[i];
        if v < 0 {
            // 如果其中任一元素为负,就进行修改
            input.to_mut()[i] = -v;
        }
    }
}

let slice = [0, 1, 2];
// `Cow<'a, [T]>` 实现了 `From<&'a [T]>`
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);
// `Cow` 依然是 `Borrowed`
// 没有克隆发生,因为 `Cow` 是惰性的,且 `input` 无需进行修改

let slice = [-1, 0, 1];
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);
// `Cow` 现在是 `Owned`
// 因为有克隆发生

// `Cow<'a, [T]>` 实现了 `From<&'a Vec<T>>`
let mut input = Cow::from(vec![-1, 0, 1]);
abs_all(&mut input);
// `Cow` 初始化时就是 `Owned`
// 所以虽然有修改操作,但是变体没有变化

Cow 实现了 Deref,这意味着你可以直接对封装的数据 B 调用其接收 &B 的方法。如果需要修改数据,调用 to_mut 方法将获取一个指向自有值的可变引用,并在必要时进行克隆。

下面是一个在结构体中使用 Cow 的例子:

use std::borrow::Cow;

struct Items<'a, X> where [X]: ToOwned<Owned = Vec<X>> {
    values: Cow<'a, [X]>, // 一个 `Cow` 字段
}

impl<'a, X: Clone + 'a> Items<'a, X> where [X]: ToOwned<Owned = Vec<X>> {
    fn new(v: Cow<'a, [X]>) -> Self {
        Items { values: v }
    }
}

// 从切片的引用类型创建一个容器
let readonly = [1, 2];
let borrowed = Items::new((&readonly[..]).into());
match borrowed {
    Items { values: Cow::Borrowed(b) } => println!("字段内部是 Borrowed 变体,借用值为 {b:?}"),
    _ => panic!("expect borrowed value"),
}

let mut clone_on_write = borrowed;
// 调用 `to_mut` 方法,会触发克隆并且返回一个指向自有值的可变引用
// 然后对可变引用进行了修改
clone_on_write.values.to_mut().push(3);
println!("clone_on_write 的值现在是 {:?}", clone_on_write.values);

match clone_on_write {
    Items { values: Cow::Owned(_) } => println!("字段内部现在是 Owned 变体"),
    _ => panic!("expect owned data"),
}
// 输出
// 字段内部是 Borrowed 变体,借用值为 [1, 2]
// clone_on_write 的值现在是 [1, 2, 3]
// 字段内部现在是 Owned 变体

如果你需要引用计数指针,Rc::make_mutArc::make_mut 同样可以提供写时克隆的功能。

参考资料