将智能指针视为具有 Deref 特征的常规引用
实现 Deref trait 允许您自定义
dereference 运算符*(不要与 multiplication 或 glob 运算符混淆)。通过以将智能指针视为常规引用的方式实现 Deref,您可以编写对引用进行作的代码,也可以将该代码与智能指针一起使用。
首先,让我们看看 dereference 运算符如何处理常规引用。然后,我们将尝试定义一个行为类似于 Box<T> 的自定义类型,并了解为什么取消引用运算符在我们新定义的类型上不能像引用那样工作。我们将探索实现 Deref trait 如何使智能指针以类似于引用的方式工作。然后我们将看看 Rust 的 deref 强制功能,以及它如何让我们使用引用或智能指针。
注意:我们将要构建的 MyBox<T> 类型与真正的 Box<T> 类型之间有一个很大的区别:我们的版本不会将其数据存储在堆上。我们将此示例的重点放在 Deref 上,因此数据的实际存储位置不如指针式行为重要。
将指针指向值
常规引用是一种指针,将指针视为指向存储在其他位置的值的箭头。在示例 15-6 中,我们创建了一个对 i32 值的引用,然后使用 dereference 运算符来跟踪对该值的引用:
文件名: src/main.rs
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
示例 15-6:使用 dereference 运算符跟随对 i32 值的引用
变量 x 的 i32 值为 5。我们将 y 设置为等于对
x. 我们可以断言 x 等于 5。但是,如果我们想对 y 中的值进行断言,我们必须使用 *y 来跟踪对它所指向的值的引用(因此取消引用),以便编译器可以比较实际值。一旦我们取消引用 y,我们就可以访问整数值
y 指向我们可以与 5 进行比较。
如果我们尝试编写 assert_eq!(5, y);相反,我们会得到这个编译错误:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
不允许将数字与引用进行比较,因为它们是不同的类型。我们必须使用 dereference 运算符来跟踪对它所指向的值的引用。
像参考一样使用 Box<T>
我们可以重写示例 15-6 中的代码,使用 Box<T> 而不是引用;示例 15-7 中 Box<T> 上使用的取消引用运算符的作用与示例 15-6 中引用上使用的取消引用运算符相同:
文件名: src/main.rs
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
示例 15-7:在
盒子<i32>
示例 15-7 和示例 15-6 的主要区别在于,这里我们设置了
y 是指向 x 的复制值的 Box<T> 的实例,而不是指向 x 值的引用。在最后一个断言中,我们可以使用 dereference 运算符来跟踪 Box<T> 的指针,就像我们在 y 是引用时所做的那样。接下来,我们将探讨 Box<T> 的特别之处,它使我们能够通过定义自己的类型来使用取消引用运算符。
定义我们自己的智能指针
让我们构建一个类似于标准库提供的 Box<T> 类型的智能指针,以体验智能指针在默认情况下的行为与引用的行为有何不同。然后,我们将了解如何添加使用 dereference 运算符的功能。
Box<T> 类型最终被定义为一个带有一个元素的元组结构体,因此示例 15-8 以相同的方式定义了 MyBox<T> 类型。我们还将定义一个
new 函数以匹配在 Box<T> 上定义的 new 函数。
文件名: src/main.rs
struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() {}
示例 15-8:定义 MyBox<T> 类型
我们定义了一个名为 MyBox 的结构体,并声明了一个泛型参数 T,因为我们希望我们的类型包含任何类型的值。MyBox 类型是一个元组结构体,其中包含一个 T 类型的元素。MyBox::new 函数采用一个 T 类型的参数,并返回一个保存传入值的 MyBox 实例。
让我们尝试将示例 15-7 中的 main 函数添加到示例 15-8 中,并将其更改为使用我们定义的 MyBox<T> 类型,而不是 Box<T>。这
示例 15-9 中的代码无法编译,因为 Rust 不知道如何取消引用
我的盒子。
文件名: src/main.rs
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);
}
示例 15-9:尝试以我们使用引用和 Box<T> 的相同方式使用 MyBox<T>
这是生成的编译错误:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
我们的 MyBox<T> 类型无法取消引用,因为我们尚未在类型上实现该功能。要使用 * 运算符启用取消引用,我们实现了 Deref trait。
通过实现 Deref trait 将类型视为引用
正如第 10 章的 “在类型上实现 trait” 部分所讨论的,要实现 trait,我们需要为 trait 的 required 方法提供实现。标准库提供的 Deref trait 要求我们实现一个名为 deref 的方法,该方法借用 self 并返回对内部数据的引用。示例 15-10 包含一个 Deref 的实现,可以添加到 MyBox 的定义中:
文件名: src/main.rs
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } 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); }
示例 15-10:在 MyBox<T> 上实现 Deref
类型 Target = T; 语法定义 Deref 的关联类型
trait 一起使用。关联类型是声明
generic 参数,但您现在无需担心它们;我们将涵盖
它们在第 19 章中有更详细的解释。
我们用 &self.0 填充 deref 方法的主体,这样 deref 就会返回一个对我们想用 * 运算符访问的值的引用;从
“使用不带命名字段的元组结构创建不同的
Types“部分,其中 .0 访问元组结构中的第一个值。示例 15-9 中对 MyBox<T> 值调用 * 的主函数现在可以编译,并且断言通过!
如果没有 Deref trait,编译器只能取消引用 & 引用。deref方法使编译器能够获取实现Deref的任何类型的值,并调用deref方法来获取它知道如何取消引用的&引用。
当我们在示例 15-9 中输入 *y 时,Rust 在幕后实际上运行了这段代码:
*(y.deref())
Rust 将 * 运算符替换为对 deref 方法的调用,然后是普通的 dereference,因此我们不必考虑是否需要调用 deref 方法。这个 Rust 功能让我们编写代码,将
无论我们有一个常规的引用还是一个实现
Deref 的 Deref 中。
deref 方法返回对值的引用,并且 *(y.deref()) 中括号外的普通取消引用仍然是必需的,这与所有权系统有关。如果 deref 方法返回值
而不是对值的引用,而是将该值从
self 中。在这种情况下,或者在大多数情况下,我们不想在 MyBox<T> 中拥有内部值的所有权,因为我们使用 dereference 运算符。
请注意,每次我们在代码中使用 * 时,* 运算符都会替换为对 deref 方法的调用,然后只调用一次 * 运算符。因为 * 运算符的替换不是无限递归的,所以我们最终会得到 i32 类型的数据,它与示例 15-9 中 assert_eq! 中的 5 匹配。
使用函数和方法的隐式 Deref 强制转换
Deref 强制将引用转换为实现 Deref 的类型
trait 转换为对另一种类型的引用。例如,deref 强制转换
&String 更改为 &str,因为 String 实现了 Deref trait,使其返回 &str。Deref 强制转换是 Rust 对函数和方法的参数执行的一种便利,并且仅适用于实现 Deref 的类型
特性。当我们将引用传递给特定类型的
value 作为与参数不匹配的函数或方法的参数
键入函数或方法定义。对 deref 的一系列调用
method 将我们提供的类型转换为参数所需的类型。
Deref 强制转换已添加到 Rust 中,因此编写函数和方法调用的程序员不需要添加尽可能多的显式引用和带有 & 和 * 的取消引用。deref 强制功能还允许我们编写更多可用于引用或智能指针的代码。
为了看到 deref 强制转换的实际效果,让我们使用示例 15-8 中定义的 MyBox<T> 类型,以及我们在示例 15-10 中添加的 Deref 实现。示例 15-11 显示了一个具有字符串 slice 参数的函数的定义:
文件名: src/main.rs
fn hello(name: &str) { println!("Hello, {name}!"); } fn main() {}
示例 15-11:一个 hello 函数,其参数为
类型 &str 的名称
我们可以用字符串 slice 作为参数来调用 hello 函数,比如
hello(“Rust”);例如。Deref 强制转换使调用 hello 成为可能
引用 MyBox<String> 类型的值,如示例 15-12 所示:
文件名: src/main.rs
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); }
示例 15-12:使用对
MyBox<String> 值,由于取消引用强制转换而有效
在这里,我们使用参数 &m 调用 hello 函数,该参数是对 MyBox<String> 值的引用。因为我们在示例 15-10 中在 MyBox<T> 上实现了 Deref trait,所以 Rust 可以将 &MyBox<String> 转换为 &String
通过调用 deref。标准库提供了 Deref 的实现
on String 返回字符串切片,这在 Deref 的 API 文档中。Rust 再次调用 deref 将 &String 转换为 &str,这与 hello 函数的定义匹配。
如果 Rust 没有实现解引用强制转换,我们将不得不编写示例 15-13 中的代码,而不是示例 15-12 中的代码来调用值为 &MyBox<String> 的 hello。
文件名: src/main.rs
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); }
示例 15-13:如果 Rust 没有 deref 强制转换,我们必须编写的代码
(*m) 将 MyBox<String> 取消引用为 String。然后是 & 和
[..]获取 String 的 String 切片,该切片等于整个字符串,以匹配 hello 的签名。这段没有 deref 强制转换的代码在涉及所有这些符号的情况下更难阅读、写入和理解。Deref 强制允许 Rust 自动为我们处理这些转换。
当为所涉及的类型定义 Deref trait 时,Rust 将分析类型并根据需要多次使用 Deref::d eref 来获得匹配参数类型的引用。Deref::d eref 需要插入的次数在编译时解决,因此利用 deref 强制转换不会造成运行时损失!
Deref Coercion 如何与可变互
与使用 Deref trait 覆盖不可变引用上的 * 运算符类似,你可以使用 DerefMut trait 覆盖 *
运算符。
Rust 在三种情况下找到类型和 trait 实现时会进行 deref 强制转换:
当 T 时从&T到&U:解压<Target=U>
当 T 时,从&mut T到&mut U:DerefMut<Target=U>
当 T 时从&mut T到&U:解压<Target=U>
前两种情况彼此相同,只是第二种情况实现了可变性。第一种情况表明,如果您有 &T 和 T
将 Deref 实现为某种类型 U,你可以透明地得到 &U。第二种情况指出,可变引用也会发生相同的 deref 强制转换。
第三种情况更棘手: Rust 也会强制可变引用为不可变引用。但反之是不可能的:不可变引用永远不会强制可变引用。由于借用规则,如果你有一个可变引用,则该可变引用必须是对该数据的唯一引用(否则,程序将无法编译)。将一个可变引用转换为一个不可变引用永远不会违反借用规则。将不可变引用转换为可变引用需要初始不可变引用是该数据的唯一不可变引用,但借用规则并不能保证这一点。因此,Rust 不能假设将不可变引用转换为可变引用是可能的。