概述
学习 Box<T> 之后,很自然会遇到一个问题:
为什么 Box<T> 明明不是普通引用,却能像引用一样使用 * 解引用?
答案就在 Deref trait。
Deref 的作用可以先记成一句话:
它告诉编译器,一个自定义类型应该怎样表现得像引用。
这也是为什么很多智能指针虽然本质上是结构体,但用起来却很像普通引用。
先从普通引用开始
先看普通引用上 * 的含义:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
这里:
x是一个i32y是x的引用,也就是&i32*y表示沿着这个引用,取到它指向的值
所以 * 在这里就是解引用(dereference)。
如果直接写:
assert_eq!(5, y);
就会报错,因为:
5的类型是i32y的类型是&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())
也就是分成两步:
- 先调用
deref(),拿到一个引用 - 再对这个引用做普通解引用
所以 * 并不是魔法,它只是借助 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>变成&StringString自己也实现了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 原本只为引用准备的使用方式。
你真正需要掌握的是这几个递进关系:
- 普通引用可以用
* Box<T>也可以用*- 自定义类型默认不行
- 实现
Deref之后就可以了 - 实现了
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 - 再通过
String的Deref继续变成&str
最终就匹配上了 hello(name: &str)。
第 6 题答案
因为可变引用本身拥有更强的访问权限,所以把 &mut T 临时当成 &U 使用不会破坏借用规则;但不可变引用不能被升级成可变引用,否则就可能在存在其他共享借用的情况下产生独占可变借用,这会违反 Rust 的借用规则。