将智能指针当作常规引用处理
实现 Deref trait 允许你自定义解引用运算符 *(不要与乘法或通配符运算符混淆)的行为。通过以这样的方式实现 Deref,使智能指针可以被当作常规引用处理,你就可以编写对引用进行操作的代码,并且也能将该代码用于智能指针。
让我们先看看解引用运算符如何与常规引用配合工作。然后,我们将尝试定义一个行为类似 Box<T> 的自定义类型,并看看为什么解引用运算符在我们新定义的类型上不能像引用那样工作。我们将探索实现 Deref trait 如何使智能指针能够以类似于引用的方式工作。然后,我们将看看 Rust 的解引用强制转换(deref coercion)特性,以及它如何让我们同时处理引用和智能指针。
跟随引用到达值
常规引用是一种指针,理解指针的一种方式是把它看作一支指向存储在别处的值的箭头。在 Listing 15-6 中,我们创建了一个对 i32 值的引用,然后使用解引用运算符来跟随该引用到达值。
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
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}`
= help: the following other types implement trait `PartialEq<Rhs>`:
f128
f16
f32
f64
i128
i16
i32
i64
and 8 others
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
不允许将一个数字和一个对数字的引用进行比较,因为它们是不同的类型。我们必须使用解引用运算符来跟随引用到达它所指向的值。
像引用一样使用 Box<T>
我们可以重写 Listing 15-6 中的代码,使用 Box<T> 而非引用;在 Listing 15-7 中对 Box<T> 使用的解引用运算符,与 Listing 15-6 中对引用使用的解引用运算符功能相同。
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Box<i32> 上使用解引用运算符Listing 15-7 与 Listing 15-6 的主要区别在于,这里我们将 y 设置为一个指向 x 拷贝值的 box 实例,而不是一个指向 x 值的引用。在最后一个断言中,我们可以像 y 是引用时那样,使用解引用运算符来跟随 box 的指针。接下来,我们将通过定义自己的 box 类型来探索 Box<T> 有什么特别之处,使其能够使用解引用运算符。
定义我们自己的智能指针
让我们构建一个类似于标准库提供的 Box<T> 类型的包装类型,来体验智能指针类型在默认情况下与引用的行为有何不同。然后,我们将看看如何添加使用解引用运算符的能力。
注意:我们即将构建的 MyBox<T> 类型与真正的 Box<T> 之间有一个很大的区别:我们的版本不会将其数据存储在堆上。我们将本示例的重点放在 Deref 上,因此数据实际存储在哪里不如指针式行为重要。
Box<T> 类型最终被定义为一个带有一个元素的元组结构体,因此 Listing 15-8 以同样的方式定义了 MyBox<T> 类型。我们还将定义一个 new 函数,以匹配在 Box<T> 上定义的 new 函数。
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
MyBox<T> 类型我们定义了一个名为 MyBox 的结构体,并声明了一个泛型参数 T,因为我们希望我们的类型能持有任意类型的值。MyBox 类型是一个带有一个类型为 T 的元素的元组结构体。MyBox::new 函数接受一个类型为 T 的参数,并返回一个持有传入值的 MyBox 实例。
让我们尝试将 Listing 15-7 中的 main 函数添加到 Listing 15-8 中,并将其改为使用我们定义的 MyBox<T> 类型而非 Box<T>。Listing 15-9 中的代码无法编译,因为 Rust 不知道如何解引用 MyBox。
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);
}
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);
| ^^ can't be dereferenced
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 的必需方法提供实现。标准库提供的 Deref trait 要求我们实现一个名为 deref 的方法,该方法借用 self 并返回一个对内部数据的引用。Listing 15-10 包含了一个要添加到 MyBox<T> 定义中的 Deref 实现。
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);
}
MyBox<T> 上实现 Dereftype Target = T; 语法为 Deref trait 定义了一个要使用的关联类型。关联类型是一种略微不同的声明泛型参数的方式,但你现在不必担心它们;我们将在第 20 章中更详细地介绍它们。
我们用 &self.0 填充 deref 方法的主体,这样 deref 就会返回一个对我们想用 * 运算符访问的值的引用;回顾第 5 章「使用元组结构体创建不同类型」中提到,.0 访问元组结构体中的第一个值。Listing 15-9 中对 MyBox<T> 值调用 * 的 main 函数现在可以编译了,断言也通过了!
如果没有 Deref trait,编译器只能解引用 & 引用。deref 方法使编译器能够获取任何实现了 Deref 的类型的值,并调用 deref 方法来获得一个它知道如何解引用的引用。
当我们在 Listing 15-9 中输入 *y 时,Rust 在幕后实际上运行了这段代码:
*(y.deref())
Rust 将 * 运算符替换为对 deref 方法的调用,然后进行一次普通的解引用,这样我们就不必考虑是否需要调用 deref 方法。Rust 的这一特性让我们可以编写无论拥有常规引用还是实现了 Deref 的类型都能以相同方式运行的代码。
deref 方法返回对值的引用,而 *(y.deref()) 中括号外的普通解引用仍然是必要的,其原因与所有权系统有关。如果 deref 方法直接返回值而不是对值的引用,该值将被移出 self。在本例以及大多数使用解引用运算符的情况下,我们不想取得 MyBox<T> 内部值的所有权。
请注意,每次我们在代码中使用 * 时,* 运算符只会被替换为对 deref 方法的调用,然后再调用一次 * 运算符。因为 * 运算符的替换不会无限递归,所以我们最终得到的是 i32 类型的数据,这与 Listing 15-9 中 assert_eq! 里的 5 相匹配。
在函数和方法中使用解引用强制转换
解引用强制转换(deref coercion)将一个对实现了 Deref trait 的类型的引用转换为对另一个类型的引用。例如,解引用强制转换可以将 &String 转换为 &str,因为 String 实现了 Deref trait,使其返回 &str。解引用强制转换是 Rust 对函数和方法参数执行的一种便利操作,它只对实现了 Deref trait 的类型有效。当我们将一个对特定类型值的引用作为参数传递给一个函数或方法,而该函数或方法定义中的参数类型与之不匹配时,就会自动发生。一系列对 deref 方法的调用会将我们提供的类型转换为参数所需的类型。
解引用强制转换被添加到 Rust 中,是为了让编写函数和方法调用的程序员不必添加那么多显式的引用和解引用符号 & 和 *。解引用强制转换特性还让我们可以编写更多既能用于引用又能用于智能指针的代码。
为了看到解引用强制转换的实际效果,让我们使用 Listing 15-8 中定义的 MyBox<T> 类型以及 Listing 15-10 中添加的 Deref 实现。Listing 15-11 展示了一个带有字符串切片参数的函数定义。
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
name 类型为 &str 的 hello 函数我们可以用字符串切片作为参数调用 hello 函数,例如 hello("Rust");。解引用强制转换使得用一个对 MyBox<String> 类型值的引用调用 hello 成为可能,如 Listing 15-12 所示。
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);
}
MyBox<String> 值的引用调用 hello,这之所以能工作是因为解引用强制转换这里我们用参数 &m 调用 hello 函数,&m 是一个对 MyBox<String> 值的引用。因为我们在 Listing 15-10 中为 MyBox<T> 实现了 Deref trait,Rust 可以通过调用 deref 将 &MyBox<String> 转换为 &String。标准库在 String 上提供了一个 Deref 实现,它返回字符串切片,这在 Deref 的 API 文档中有说明。Rust 再次调用 deref 将 &String 转换为 &str,这与 hello 函数的定义相匹配。
如果 Rust 没有实现解引用强制转换,我们就必须编写 Listing 15-13 中的代码,而不是 Listing 15-12 中的代码,才能用 &MyBox<String> 类型的值调用 hello。
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)[..]);
}
(*m) 将 MyBox<String> 解引用为 String。然后,& 和 [..] 对 String 取一个等于整个字符串的字符串切片,以匹配 hello 的签名。这段没有解引用强制转换的代码,涉及所有这些符号,更难阅读、编写和理解。解引用强制转换允许 Rust 自动为我们处理这些转换。
当为涉及的类型定义了 Deref trait 时,Rust 会分析类型,并根据需要多次使用 Deref::deref 来获得一个与参数类型匹配的引用。需要插入 Deref::deref 的次数在编译时就已解析,因此利用解引用强制转换不会产生运行时开销!
处理可变引用的解引用强制转换
类似于你使用 Deref trait 来覆盖不可变引用上的 * 运算符,你可以使用 DerefMut trait 来覆盖可变引用上的 * 运算符。
Rust 在发现类型和 trait 实现满足以下三种情况时执行解引用强制转换:
- 从
&T到&U,当T: Deref<Target=U> - 从
&mut T到&mut U,当T: DerefMut<Target=U> - 从
&mut T到&U,当T: Deref<Target=U>
前两种情况相同,只是第二种实现了可变性。第一种情况说明,如果你有一个 &T,而 T 为某个类型 U 实现了 Deref,你就可以透明地获得一个 &U。第二种情况说明,对于可变引用也会发生相同的解引用强制转换。
第三种情况比较棘手:Rust 还会将可变引用强制转换为不可变引用。但反过来不可能:不可变引用永远不会被强制转换为可变引用。由于借用规则,如果你有一个可变引用,该可变引用必须是该数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为一个不可变引用永远不会破坏借用规则。将一个不可变引用转换为可变引用则需要最初的不可变引用是该数据的唯一不可变引用,但借用规则并不能保证这一点。因此,Rust 不能做出将不可变引用转换为可变引用是可能的这一假设。