Rust 语言特性:Deref Trait 和 解引用

我们知道:Rust 中的“引用”在底层其实是一个“指针”,它保存着一个内存中的地址,但是,和 C++ 中的裸指针不同的是,Rust 给它的“引用”类型附加了:生命周期、借用规则、可变性规则、非空保证、有效性保证等一系列的安全机制,使得 Rust 的引用要比 C++ 的指针安全得多。只要是一个引用,就可以“解引用”,获得它的值。比如像下面这样:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y); // 你不能直接比较 5 和 y,因为它们类型不一样!y 是 &i32
}
*y 中的星号就是解引用操作符。这符合大家理解的常规意义上的“解引用”行为。
1. Deref:此解引用非彼解引用
在 Rust 中有一个 Deref trait:[ 官方文档 ],这个词直译就是“解引用”的意思,那这个 Deref 是干什么用的呢?我们先看一个例子:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
在上述代码中,有一个自定义类型 MyBox<T>1,它是一个单元素的元组结构体,当我们在第 14 行试图像前面的示例一样使用解引用操作符把它里面包裹的整型值 5 取出来的时候,编译报错了:type MyBox<{integer}> cannot be dereferenced,意思是说:我们的 MyBox 类型不能被解引用。在 Rust 中,对于一个自定义类型来说,是不能像基本类型那样直接使用解引用操作符的,因为编译器并不知道应该从这个类型中取出哪些值,这需要开发者来指定,通过什么方式指定呢?就是通过实现 Deref 这个特质,在 deref() 方法中把逻辑写出来。继续以 MyBox 为例,看一下如何通过实现 Deref 来解决编译报错:
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
我们让 MyBox 实现了 Deref,在 deref() 方法中返回 &self.0,也就是 MyBox 这个 tuple 结构体的第一个元素(字段)的引用!请注意这个重要的细节:首先,deref 返回的是 &T 不是 T,也就是说它返回的不是自定义类型包裹的值,而是值的引用!这就是本小结标题的意思:此解引用非彼解引用,也就是说:虽然 deref 被翻译成“解引用”,但是它返回的并不是值,而是一个引用。再者,deref 要求的参数是一个 &self,也就是一个 MyBox 的引用,当我们对一个 MyBox 的值(实例)执行 deref 时,编译器会自动取它的引用,这一点后面马上就会看到。
实现了 Deref 特质后,原来代码中的 assert_eq!(5, *y) 就可以正常运行了,因此,很多初学者会错误地把 deref 理解成像 C++ 中的那样的“解引用操作符”重载,认为:deref 方法 == * 操作符,但这是错误的。实际情况是:当 MyBox 实现了 Deref 之后,在遇到 *y 时,编译器会在底层运行如下代码:
*(y.deref())
Rust 会先调用 y 的 deref() 方法来得到实际值的引用,然后再执行 * 运算取出里面的值!此外,还有一个需要注意的细节,就是:deref() 需要的参数其实是 &self,是关于 y 的一个引用,而不是 y 值,在执行 *y 时编译器还对 y 进行了自动取引用操作,所以完整的拆解其实是这样的:
*(Deref::deref(&y))
至于,为什么 deref() 方法返回的是 &T 而不是 T,有两个原因,第一个原因是如果 deref() 方法直接返回值而不是值的引用,其值(的所有权)将被移出 self,第二个原因是,返回 &T 再使用 * 取出引用的值刚好能和普通类型的解引用操作对齐。(一个补充知识:i32 类型并没有实现 Deref)。
2. 强制解引用转换(deref coercions)
强制解引用转换是将一种类型 A 隐式转换为另外一种类型 B 的引用,因为 A 类型实现了 Deref trait,并且其关联类型(A 包裹的值的类型)是 B 类型。比如,强制解引用转换可以将 &String 转换为 &str,因为类型 String 实现了 Deref trait 并且其关联类型是 str。强制解引用转换的加入使得 Rust 开发者编写函数和方法调用时无需过多地使用 & 和 * 操作符。这个功能也能让我们编写出更多同时作用于引用或智能指针的代码。看一个示例:
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
在上面的示例中,hello() 函数接受的参数是 &str 类型,但在调用时传给它的是一个 &MyBox<String>,从 &MyBox<String> 到 &str,这中间发生的事情是:首先:由于 MyBox<String> 实现了 Deref,这意味着它可以通过 Deref 自动转换为 &String,注意,这里有一个细节,可能有人会迷惑:在上面的例子中,deref 不是发生在 MyBox 上的的吗,那这里传入 &MyBox<String> 还能进行解引用吗?不会变成 &&String 吧?这个问题其实在上一节的示例中已经解释了,在上一个示例中 *y 在自动解引用的时候,还有一个隐式动作,就是自动给 y 进行了“取引用”操作,以适配 deref(&self) 的函数签名。而在这里本来给出的就是 &MyBox<String>,是完全匹配解引用函数 deref(&self) 的签名。我们继续,通过 Deref,&MyBox<String> 将变成 &String,而 String 又实现了到 &str 的 Deref,参见《官方文档》,也就是说:经过两次自动解引用(强制解引用转换),&MyBox<String> 变成了 &str,这中间是一条丝滑的“调用链”,编译器可以连续在返回类型上进行解引用操作并最终适配到目标类型。从这个角度来看,强制解引用转换并不是真得“强制转换”,而是通过自动解引用取出当前类型“包裹”的值的引用,然后再对值的引用继续进行解引用,取出它包裹的值的引用,依次类推下去,直至得到目标类型的引用(如果 match 的话),这个过程发生在编译期,不影响运行时性能。
如果没有强制解引用转换,我们将不得不手动编写转化代码,它是将会是下面的样子(前提是 MyBox 已经实现了 Deref ):
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
首先:*m 操作会将 MyBox<String> 解引用为 String, 实际发生的动作是:*(Deref::deref(&m)),然后,通过 & 和 [..] 获取了整个 String 的字符串切片。从手写的复杂冗长代码可以体会到:为什么 Rust 要引入强制解引用转换,因为 Rust 中大量使用智能指针(这里是指像 MyBox 那样的广义智能指针,即内部包裹一份数据,自身充当容器,实现 Deref/DerefMut,对外暴露内部数据访问。这样就算是广义的智能指针了,不一定非要分配堆内存),如果没有强制解引用转换,开发者就必须手写大量的 * 和 &,使得代码变得繁复而丑陋,引入强制解引用转换后,作用智能指针时就会像普通类型一样,变得简洁干净。
3. 强制解引用转化中的可变性
在强制解引用转化的过程中会牵涉到转换后的引用的可变性怎么界定的问题。通常会有三种可能的情况:
- 从一个“不可变引用”强制解引用转化为一个“不可变引用”
- 从一个“可变引用”强制解引用转化为一个“可变引用”
- 从一个“可变引用”强制解引用转化为一个“不可变引用”
前两种没有改变转换后的可变与不可变性质,没有问题,第三种是收紧(变严格)了限制,所以也是允许的(因为是安全的),没有从一个“不可变引用”到一个“可变引用”的强制解引用转化,这是不安全的,不会被允许。
-
《Rust 程序设计语言》一书把 MyBox 这个自定义结构体称为:自定义智能指针,这里应该理解为一种宽泛的或广义的智能指针,也 Box 类型是有差别的。MyBox 符合智能指针的两个基本特征:1. 在它的内部包裹了数据,自己充当容器;2. 它实现了 Deref,允许解引用。但 MyBox 没有申请堆内存,和标准意义上的智能指针有一些区别。 ↩︎
更多推荐



所有评论(0)