Rust Deref:让智能指针像普通引用一样工作

 

概述

学习 Box<T> 之后,很自然会遇到一个问题:

为什么 Box<T> 明明不是普通引用,却能像引用一样使用 * 解引用?

答案就在 Deref trait。

Deref 的作用可以先记成一句话:

它告诉编译器,一个自定义类型应该怎样表现得像引用。

这也是为什么很多智能指针虽然本质上是结构体,但用起来却很像普通引用。

先从普通引用开始

先看普通引用上 * 的含义:

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

这里:

  • x 是一个 i32
  • yx 的引用,也就是 &i32
  • *y 表示沿着这个引用,取到它指向的值

所以 * 在这里就是解引用(dereference)

如果直接写:

assert_eq!(5, y);

就会报错,因为:

  • 5 的类型是 i32
  • y 的类型是 &i32

它们不是同一个类型,不能直接比较。


Box<T> 为什么也能用 *

再看 Box<T>

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

这里的 y 不是 &i32,而是 Box<i32>

*y 依然成立。

这说明一件事:

Rust 并不是只允许对普通引用使用 *,而是允许对“实现了相应能力的类型”使用 *

对于 Box<T> 来说,这个能力就来自 Deref trait。


自定义一个“像 Box 一样”的类型

为了真正理解 Deref,最好的方式不是直接背定义,而是自己写一个最小版本。

struct MyBox<T>(T);

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

这里的 MyBox<T> 是一个元组结构体,本质上只是把一个值包了起来。

例如:

fn main() {
    let x = 5;
    let y = MyBox::new(x);
}

现在问题来了:如果写 *y 会怎样?

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

这段代码不能编译

原因很简单:

虽然 MyBox<T> 看起来像是把值包起来了,但 Rust 并不会自动认为它就是“可解引用的指针类型”。

也就是说:

“长得像盒子”不等于“编译器知道怎么解引用它”。


Deref trait 到底做了什么

如果希望 MyBox<T> 也支持 *,就需要为它实现 Deref trait:

use std::ops::Deref;

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

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

这段实现里最关键的是两点:

  • type Target = T;
  • fn deref(&self) -> &Self::Target

Target 是什么

Target 表示:这个类型解引用之后,目标类型是什么。

这里写成:

type Target = T;

意思就是:

MyBox<T> 解引用后,得到的是里面那个 T

deref 返回什么

deref 方法必须返回一个引用

&self.0

这里的 .0 表示取元组结构体的第一个字段。

所以这段代码的意思就是:

当别人想解引用 MyBox<T> 时,请把内部值的引用交出去。


为什么 deref 返回的是引用

这是这一节很容易忽略,但其实非常重要的一点。

deref 返回的是:

&T

而不是:

T

原因是如果直接返回 T,就相当于把内部值从 self 里移出来了。

这通常不是我们想要的行为。

解引用的目标是:

  • 访问内部值
  • 像借用一样使用它

而不是每用一次 * 就把拥有权拿走。

所以 Deref 的核心不是“取出值”,而是:

提供一个指向内部值的引用。


*y 背后到底发生了什么

当你写:

*y

如果 y 的类型实现了 Deref,Rust 在底层会把它理解成:

*(y.deref())

也就是分成两步:

  1. 先调用 deref(),拿到一个引用
  2. 再对这个引用做普通解引用

所以 * 并不是魔法,它只是借助 Deref trait 把“自定义类型”桥接成“普通引用”。


Deref 的真正价值

如果没有 Deref,那么自定义智能指针就只是“包了一层值的结构体”,用起来会非常别扭。

实现了 Deref 后,它才能在很多地方表现得像引用:

  • 能使用 *
  • 能更自然地传给需要引用的代码
  • 能参与 Deref 强制转换

所以 Deref 的意义不是单纯“支持一个运算符”,而是:

让智能指针真正融入 Rust 的引用体系。


什么是 Deref 强制转换

这是 Deref 最实用的一部分。

先看一个函数:

fn hello(name: &str) {
    println!("Hello, {name}!");
}

它需要的是 &str

如果我们现在有一个 MyBox<String>

let m = MyBox::new(String::from("Rust"));

然后这样调用:

hello(&m);

这居然也是合法的。

为什么?

因为 Rust 会自动做一系列 Deref 强制转换:

&MyBox<String>
-> &String
-> &str

也就是说:

  • MyBox<T>Deref&MyBox<String> 变成 &String
  • String 自己也实现了 Deref<Target = str>
  • 所以 Rust 还能继续把 &String 变成 &str

最终就匹配上了 hello 的参数类型。


如果没有 Deref 强制转换会怎样

如果 Rust 不帮你做这件事,代码就会变得更难读:

hello(&(*m)[..]);

这里做了很多手动操作:

  • *m:先解引用成 String
  • [..]:再取整个字符串切片
  • &:最后再借用成 &str

程序当然也能写,但可读性明显更差。

所以 Deref 强制转换本质上是在帮你做:

那些语义明确、而且编译器可以安全推导的引用转换。


Deref 强制转换只发生在什么地方

最重要的结论是:

Deref 强制转换主要发生在函数和方法传参时。

也就是说,当你提供的是某种“实现了 Deref 的引用”,而目标参数需要的是另一种引用时,Rust 会尝试自动转换。

这也是为什么它被称为一种“便利机制”。

它不会把整个类型系统变得随意,而是只在安全、明确的场景里帮你少写很多 &* 和切片语法。


DerefMut 和可变引用

Deref 类似,Rust 还提供了 DerefMut,用于处理可变引用的解引用行为。

可以先记住三个常见规则:

  • T: Deref<Target = U> 时,可以从 &T 转成 &U
  • T: DerefMut<Target = U> 时,可以从 &mut T 转成 &mut U
  • T: Deref<Target = U> 时,也可以从 &mut T 转成 &U

最后这一条容易误解,但本质并不奇怪:

  • 可变引用可以暂时当成不可变引用用
  • 反过来则不行

因为:

不可变引用不能凭空升级成可变引用。

这会破坏借用规则。


学这一节时最该抓住的主线

如果把这一节压缩成一条主线,其实就是:

Deref 让“像指针的自定义类型”能够接入 Rust 原本只为引用准备的使用方式。

你真正需要掌握的是这几个递进关系:

  1. 普通引用可以用 *
  2. Box<T> 也可以用 *
  3. 自定义类型默认不行
  4. 实现 Deref 之后就可以了
  5. 实现了 Deref 后,还能触发 Deref 强制转换

只要这条主线清楚了,这一节基本就通了。


小结

Deref trait 的核心价值,不是“多了一个语法技巧”,而是:

  • 让智能指针像普通引用一样工作
  • * 能作用于自定义智能指针
  • 让函数和方法参数中的引用转换更自然
  • 让包装类型在使用时不至于太笨重

如果用一句话总结:

Deref 负责告诉 Rust:当别人把我当引用用时,应该怎样拿到我内部真正想暴露的那个值。


知识自测

尝试独立回答下面这些课后习题,看看这一节的关键点是否已经真正掌握。


第 1 题:为什么普通引用 &T 在比较或访问底层值时,经常需要配合 * 解引用?

第 2 题:为什么 Box<T> 可以使用 *,而我们自己定义的 MyBox<T> 在默认情况下却不行?

第 3 题Deref trait 中的 deref(&self) -> &Self::Target 为什么必须返回引用,而不是直接返回值?

第 4 题:写出 *y 在实现了 Deref 的类型上的底层等价形式,并解释这说明了什么。

第 5 题:什么是 Deref 强制转换?为什么 hello(&m) 可以在 m: MyBox<String> 的情况下匹配到参数类型 &str

第 6 题:为什么 Rust 允许把 &mut T 转成 &U,却不允许把 &T 转成 &mut U


📖 点击查看答案

第 1 题答案

因为引用本身和它指向的值不是同一个东西。&T 是一个引用,* 的作用是沿着引用拿到它所指向的底层值,所以像 assert_eq!(5, *y) 这样的写法才能真正比较到底层数据。


第 2 题答案

因为 Box<T> 实现了 Deref trait,编译器知道该怎样把它当成“可解引用的类型”处理;而 MyBox<T> 只是一个普通结构体,默认不会自动获得这种能力。只有实现了 Deref*y 才能成立。


第 3 题答案

因为 Deref 的目标是“借用内部值”,而不是把内部值从 self 中移出去。如果直接返回值,就可能导致所有权被移动,这不符合解引用通常只是访问数据的预期。返回引用既安全,也符合引用语义。


第 4 题答案

底层可以理解成:

*(y.deref())

这说明 * 并不是只认普通引用。对于实现了 Deref 的类型,Rust 会先调用 deref(),拿到一个普通引用,再继续执行真正的解引用。


第 5 题答案

Deref 强制转换是 Rust 在函数或方法传参时自动做的引用类型转换。对于 hello(&m) 而言:

  • &m 的类型是 &MyBox<String>
  • 通过 MyBox<T>Deref 先变成 &String
  • 再通过 StringDeref 继续变成 &str

最终就匹配上了 hello(name: &str)


第 6 题答案

因为可变引用本身拥有更强的访问权限,所以把 &mut T 临时当成 &U 使用不会破坏借用规则;但不可变引用不能被升级成可变引用,否则就可能在存在其他共享借用的情况下产生独占可变借用,这会违反 Rust 的借用规则。


参考资料