Rust 智能指针 I

你应该知道的特征

技术
技术Rust标准库编程语言特性特征

2026-03-29

Rust 中的引用可以说无处不在。对于常规引用来说,它就是一个存储了目标数据内存地址的指针类型,对常规引用使用 * 操作符,就可以通过解引用的方式获取到内存地址对应的数据值。这和像 C 那样带有指针的编程语言大概没什么两样,但在 Rust 中常规引用还有另一层含义:某值的引用相当于对它的借用。引用本身很简单,除了指向某个值外并没有其它的功能,也不会造成性能上的额外损耗。

而智能指针则不然,它虽然也号称指针,但是它是一个更复杂的家伙:通过应用比常规引用更复杂的数据结构,从而包含更多的信息。另外,智能指针往往拥有它们指向的数据,而不像引用那样只是对所指数据的借用。这种对数据的所有权让智能指针的灵活性和功能性更为强大。

定义智能指针行为的两种特征

智能指针往往都实现了 DerefDrop 特征。了解这两种特征对理解后面那些智能指针的行为大有裨益。

定义解引用行为的 Deref

前面提过,对常规引用使用 * 操作符,就可以通过解引用获取到内存地址对应的数据值。

而智能指针并不像引用那样纯粹。事实上智能指针是一个结构体,如果我像对引用那样对一个结构体直接解引用,那 Rust 肯定会不知所措吧:解引用后提供访问的应该是结构体的哪个字段?是栈上的数据还是堆上的?……

Deref 就是让智能指针像引用那样工作的法宝。下面是定义:

pub trait Deref: PointeeSized {
    type Target: ?Sized;

    // Required method
    fn deref(&self) -> &Self::Target;
}

该特征控制着称为解引用转换的行为。

解引用转换(Deref coercion)是指如果类型 T 实现了 Deref<Target = U>,且 v 是类型为 T 的值,那么:

  1. 在不可变的上下文中,*v(其中 T 既不是引用类型也不是裸指针)等价于 *Deref::deref(&v)
  2. &T 类型的值会被转换为 &U 类型的值。
  3. T 会隐式地实现类型 U 中所有接收 &self 的方法。

上面三条法则听起来很绕,对吗?

让我们通过更清晰的实例来逐条了解它们。


实现 Deref 后的智能指针结构体,就可以像普通引用一样通过 * 解引用来访问其数据。

// `Box` 就是一种简单的智能指针
let x = Box::new(1);
let sum = *x + 1;

Deref 在背后所做的,就是在你对智能指针使用 * 时先调用 deref 方法来返回指向值的常规引用,然后通过你的解引用获取到想获取的数据。

let sum = *x + 1;
// 实际是 
let sum = *(x.deref()) + 1;

Rust 之所以这么设计,是因为所有权系统的存在。

如果 deref 方法直接返回值而不是引用,那么该值的所有权将被转移给调用者。前面提到智能指针一般都具有对数据的所有权,如果你只靠简简单单的 * 解引用就能把智能指针里的数据取走,那这智能指针也太不可靠了吧!

像引用一样通过 * 解引用智能指针,这一行为如何体现那三条法则?

第 1 条法则说:在不可变的上下文中,*v 等价于 *Deref::deref(&v)。让我们忽略前半句。

例子中的 x 就是法则中的 v*x 因此等价于 *Deref::deref(&x),也就是 *(&x).deref()。如果你还记得 deref 方法的签名,就知道它接收的是 &T。因此 *(&x).deref() 就相当于 *(x.deref())


Rust 还提供了一种极其有用的机制:隐式 Deref 转换。

若一个类型实现了 Deref 特征,那它的引用在传入函数或方法时,会根据参数签名来决定是否进行隐式的 Deref 转换。并且这种隐式转换可以连续进行多次,直至匹配参数签名。

fn main() {
    let s = String::from("hello world");
    display(&s);
    // `display` 函数要求传入的是 `&str`
    // 但实际是 `&String`
    // 由于 `String` 实现了 `Deref`,且
    // `deref` 方法接收 `&String` 返回 `&str`
    // 所以这个语句等价于
    // display((&s).deref());
}

fn display(s: &str) {
    println!("{}", s);
}

要想触发隐式 Deref 转换,必须保证作为参数传入的是引用类型。

也就是说上面的例子必须是 display(&s),而不能是 display(s)

例子中的隐式 Deref 转换,这一行为如何体现那三条法则?

如果你查看文档中 String 类型的实现,就会看到类似这样的:

impl Deref for String {
    type Target = str;

    fn deref(&self) -> &str {
        /* 忽略实现细节 */
    }
}

也就是说法则中的类型 U 就是 str

第 2 条法则说:&T 类型的值会被转换为 &U 类型的值。意思就是 &String 会被转为 &str,这也正是示例所展示给我们的。

而我补充的“触发隐式 Deref 转换必须保证传入的是引用”就可以理解了,因为法则说只有 &T 类型的值会被转换,如果传入 T 自然就无法隐式转换了。

涉及可变引用的解引用行为

之前,我们讲的都是不可变的 Deref 转换,实际上 Rust 还支持将一个可变引用转换成另一个可变引用以及将一个可变引用转换成不可变引用,实现这两种转换的分别是 DerefMutDeref


DerefMut 定义如下。可见,实现 DerefMut 必须先实现 Deref

pub trait DerefMut: Deref + PointeeSized {
    // Required method
    fn deref_mut(&mut self) -> &mut Self::Target;
}

前面我们对智能指针不管是进行解引用还是隐式转换,都只是在“访问”而非“修改”数据。回到第一个例子,如果我们想实现类似:

let x = Box::new(1);
*x = 1;

就需要了解 DerefMut 及可变 Deref 转换。

可变解引用转换(Mutable deref coercion)是指如果类型 T 实现了 DerefMut<Target = U>,且 v 是类型为 T 的值,那么:

  1. 在可变的上下文中,*v(其中 T 既不是引用类型也不是裸指针)等价于 *DerefMut::deref_mut(&v)
  2. &mut T 类型的值会被转换为 &mut U 类型的值。
  3. T 会隐式地实现类型 U 中所有接收 &mut self 的方法。

看起来是不是很眼熟?其实就是把前面 Deref 转换中的“不可变”换成“可变”的版本。

这就意味着之前提到的关于 Deref 的种种使用特性在这里也同样适用,只不过数据权限从“访问”上升到了“修改”。


讲完了可变引用向可变引用的转换,那么可变引用向不可变引用如何转换呢?这就依赖 Deref 了。

T 实现了 Deref,就可以将 &mut T 转换成 &U

上面这句话隐含地说明:Rust 可以把可变引用隐式地转换成不可变引用,反之则不行。

如果从 Rust 的所有权和借用规则的角度考虑,当你拥有一个可变引用,那该引用肯定是对应数据的唯一借用,那么此时将可变引用变成不可变引用并不会破坏借用规则;但是如果你拥有一个不可变引用,那同时可能还存在其它几个不可变的引用,如果此时将其中一个不可变引用转换成可变引用,就变成了可变引用与不可变引用的共存,最终破坏了借用规则。

定义析构行为的 Drop

当一个变量离开其作用域时,Rust 会自动为其调用析构函数(destructor)以将占用的资源进行回收。这个过程通常是隐式且全面地进行的,并不需要人们为之操心。但 Rust 依旧提供了对此行为的控制权,那便是 Drop 特征。

pub trait Drop {
    // Required method
    fn drop(&mut self);
}

Rust 的自动析构已经很智能了,为什么还需要实现 Drop

这是因为析构的类型有时是很复杂的,它可能会占有着某些系统资源,譬如一块内存、一个网络连接、一个文件描述符。Rust 尽管会自动析构类型的值及其所有字段,却并不一定有能力释放这些资源。一旦类型被析构,但其占用的资源却没有释放,可想而知后果是多么严重。

实际大多数情况下,我们不需要为类型手动实现 Drop,因为隐式析构已经足够强大;但是在类似上述的特殊情况中,仍然需要我们为程序的正常运行补上缺失的一环。

Drop 特征的 drop 方法只是借用了目标的可变引用,而不是拿走了所有权。这就导致 Rust 不允许显式地调用 Drop::drop。一旦你提前释放了资源,由于 drop 不会拿走所有权,所以值还在其作用域内但已经变得无效,此时你再使用已被释放的值岂不是很危险?Rust 可不会对你的行为买单,所以任何时候都不允许显式调用 Drop::drop

当然,Rust 终究给予了程序员最大的自由度。如果你想显式地析构值,mem::drop 大概是你需要的,编译器有时也会提示你使用这个函数。因为与 Drop::drop 方法不同,它会拿走值的所有权,从而消除了上面的隐患。

互斥的 DropCopy

对同一个类型,不允许同时实现 DropCopy

由于 Copy 所定义的行为是编译器会静默地复制该类型的值(而不像显式调用 clone 那样),这就导致自动推定释放资源的时间节点变得异常困难。因此 Copy 类型不具有析构函数。

析构次序

对于处在同一作用域中的多个值,它们同时离开作用域时被析构的先后次序是:

struct Field1Drop;

impl Drop for Field1Drop {
    fn drop(&mut self) {
        println!("Dropping Field1Drop!");
    }
}

struct Field2Drop;

impl Drop for Field2Drop {
    fn drop(&mut self) {
        println!("Dropping Field2Drop!");
    }
}

struct TwoDrops {
    one: Field1Drop,
    two: Field2Drop,
}

impl Drop for TwoDrops {
    fn drop(&mut self) {
        println!("Dropping TwoDrops!");
    }
}

struct FirstDrop;

impl Drop for FirstDrop {
    fn drop(&mut self) {
        println!("Dropping FirstDrop!");
    }
}

fn main() {
    let _x = TwoDrops { one: Field1Drop, two: Field2Drop };
    let _first = FirstDrop;
    println!("Running!");
}
// 输出:
// Running!
// Dropping FirstDrop!   // 后定义的先析构
// Dropping TwoDrops!    // 先定义的后析构
// Dropping Field1Drop!  // 结构体字段 1 被析构
// Dropping Field2Drop!  // 结构体字段 2 被析构

参考资料