将智能指针像常规引用一样处理的 Deref 特性

实现 Deref 特性允许你自定义 解引用操作符 * 的行为(不要与乘法或通配符操作符混淆)。通过以这种方式实现 Deref,使得智能指针可以像普通引用一样被处理,你可以编写操作引用的代码,并且也可以用这些代码处理智能指针。

让我们首先看看解引用操作符是如何与常规引用一起工作的。 然后我们将尝试定义一个像Box<T>一样行为的自定义类型,并看看为什么 解引用操作符在我们新定义的类型上不像引用那样工作。我们将探讨实现Deref特征如何使 智能指针能够以类似于引用的方式工作。然后我们将研究 Rust的解引用强制转换特性,以及它如何让我们可以使用引用或智能指针。

注意:我们即将构建的 MyBox<T> 类型和真正的 Box<T> 之间有一个很大的不同:我们的版本不会在堆上存储数据。我们在这个例子中专注于 Deref,因此数据实际存储的位置不如指针行为重要。

跟随指针到值

一个常规引用是一种指针,而思考指针的一种方式是 将其视为指向存储在其他地方的值的箭头。在示例 15-6 中,我们创建了一个 指向 i32 值的引用,然后使用解引用运算符来跟踪 引用到值:

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-6: Using the dereference operator to follow a reference to an i32 value

变量 x 持有一个 i325。我们将 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)
help: consider dereferencing here
 --> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/macros/mod.rs:46:35
  |
46|                 if !(*left_val == **right_val) {
  |                                   +

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> 用作引用

我们可以将列表 15-6 中的代码重写为使用 Box<T> 而不是引用;在列表 15-7 中对 Box<T> 使用的解引用运算符与在列表 15-6 中对引用使用的解引用运算符功能相同:

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-7: Using the dereference operator on a Box<i32>

列表 15-7 和列表 15-6 之间的主要区别在于这里我们将 y 设置为一个指向 x 值的副本的 Box<T> 实例,而不是指向 x 值的引用。在最后一个断言中,我们可以使用解引用运算符来跟踪 Box<T> 的指针,就像 y 是引用时我们所做的那样。接下来,我们将通过定义我们自己的类型来探讨 Box<T> 的特殊之处,这使我们能够使用解引用运算符。

定义我们自己的智能指针

让我们构建一个类似于标准库提供的Box<T>类型的智能指针,以体验智能指针与引用在默认行为上的不同。然后我们将研究如何添加使用解引用运算符的能力。

Box<T> 类型最终被定义为一个包含一个元素的元组结构体,因此 列表 15-8 以相同的方式定义了一个 MyBox<T> 类型。我们还将定义一个 new 函数,以匹配在 Box<T> 上定义的 new 函数。

Filename: src/main.rs
struct MyBox<T>(T);

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

fn main() {}
Listing 15-8: Defining a MyBox<T> type

我们定义了一个名为MyBox的结构体,并声明了一个泛型参数T,因为 我们希望我们的类型能够持有任何类型的值。MyBox类型是一个包含一个T类型元素的元组结构体。MyBox::new函数接受一个T类型的参数,并返回一个持有传入值的MyBox实例。

让我们尝试将清单 15-7 中的 main 函数添加到清单 15-8 中,并将其更改为使用我们定义的 MyBox<T> 类型而不是 Box<T>。清单 15-9 中的代码将无法编译,因为 Rust 不知道如何解引用 MyBox

Filename: 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);
}
Listing 15-9: Attempting to use MyBox<T> in the same way we used references and Box<T>

以下是编译错误:Here’s the resulting compilation error:

$ 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 特性。

通过实现 Deref 特性将类型视为引用

如第 10 章的 “在类型上实现特征” 部分所述,要实现一个特征,我们需要为特征的必需方法提供实现。标准库提供的 Deref 特征要求我们实现一个名为 deref 的方法,该方法借用 self 并返回对内部数据的引用。列表 15-10 包含为 MyBox 的定义添加的 Deref 实现:

Filename: 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);
}
Listing 15-10: Implementing Deref on MyBox<T>

type Target = T; 语法为 Deref 特性定义了一个关联类型。关联类型是一种声明泛型参数的略有不同的方式,但你现在不需要担心它们;我们将在第 20 章中详细讨论。

我们用&self.0 填充 deref 方法的主体,这样 deref 返回一个引用,我们可以通过 * 操作符访问我们想要的值;回想第 5 章“使用无命名字段的元组结构体创建不同类型的实例”部分,.0 访问元组结构体中的第一个值。现在,清单 15-9 中调用 *MyBox<T> 值的 main 函数可以编译,并且断言通过!

没有 Deref 特性,编译器只能对 & 引用进行解引用。 deref 方法使编译器能够获取任何实现了 Deref 的类型的值,并调用 deref 方法以获得一个 & 引用,编译器知道如何对其进行解引用。

当我们输入 *y 在清单 15-9 中时,实际上 Rust 在幕后运行了这段代码:

*(y.deref())

Rust 用 deref 方法的调用替换了 * 操作符,然后进行普通的解引用,这样我们就不必考虑是否需要调用 deref 方法。这个 Rust 特性让我们可以编写无论拥有普通引用还是实现了 Deref 的类型都能同样工作的代码。

deref 方法返回一个值的引用,以及在 *(y.deref()) 中括号外的普通解引用仍然必要,这与所有权系统有关。如果 deref 方法直接返回值而不是值的引用,该值将从 self 中移出。在这种情况下,或者在大多数使用解引用操作符的情况下,我们不希望获取 MyBox<T> 内部值的所有权。

请注意,每次我们在代码中使用 * 时,* 操作符都会被替换为对 deref 方法的调用,然后是对 * 操作符的调用,但仅一次。因为 * 操作符的替换不会无限递归,我们最终得到的是 i32 类型的数据,这与列表 15-9 中 assert_eq! 中的 5 匹配。

函数和方法中的隐式解引用强制转换

Deref 强制转换 将对实现了 Deref 特性的类型的引用转换为对另一种类型的引用。例如,Deref 强制转换可以将 &String 转换为 &str,因为 String 实现了 Deref 特性,使其返回 &str。Deref 强制转换是 Rust 在函数和方法的参数上执行的一种便利操作,仅适用于实现了 Deref 特性的类型。当我们将对特定类型值的引用作为参数传递给函数或方法时,如果该参数类型与函数或方法定义中的参数类型不匹配,Deref 强制转换会自动发生。一系列对 deref 方法的调用将我们提供的类型转换为参数所需的类型。

Deref 强制转换被添加到 Rust 中,以便编写函数和方法调用的程序员不需要添加那么多显式的引用和解引用的 &*。Deref 强制转换特性还让我们能够编写更多可以适用于引用或智能指针的代码。

要看到解引用强制的效果,让我们使用我们在清单 15-8 中定义的 MyBox<T> 类型以及我们在清单 15-10 中添加的 Deref 实现。清单 15-11 显示了一个具有字符串切片参数的函数的定义:

Filename: src/main.rs
fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}
Listing 15-11: A hello function that has the parameter name of type &str

我们可以使用字符串切片作为参数调用 hello 函数,例如 hello("Rust");。解引用强制使得我们可以使用 MyBox<String> 类型值的引用调用 hello,如清单 15-12 所示:

Filename: 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);
}
Listing 15-12: Calling hello with a reference to a MyBox<String> value, which works because of deref coercion

这里我们调用 hello 函数,参数为 &m,这是对 MyBox<String> 值的引用。因为我们在清单 15-10 中为 MyBox<T> 实现了 Deref 特性,Rust 可以通过调用 deref&MyBox<String> 转换为 &String。标准库为 String 提供了一个返回字符串切片的 Deref 实现,这在 Deref 的 API 文档中有说明。Rust 再次调用 deref&String 转换为 &str,这与 hello 函数的定义相匹配。

如果 Rust 没有实现解引用强制,我们就必须编写列表 15-13 中的代码,而不是列表 15-12 中的代码,以使用类型为 &MyBox<String> 的值调用 hello

Filename: 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)[..]);
}
Listing 15-13: The code we would have to write if Rust didn’t have deref coercion

(*m)MyBox<String> 解引用为 String。然后 &[..]String 中获取一个等于整个字符串的字符串切片,以匹配 hello 的签名。没有解引用强制转换的代码由于涉及这些符号而更难阅读、编写和理解。解引用强制转换允许 Rust 自动为我们处理这些转换。

当为涉及的类型定义了Deref特征时,Rust将分析类型并根据需要多次使用Deref::deref以获取与参数类型匹配的引用。需要插入Deref::deref的次数在编译时确定,因此利用解引用强制转换不会带来运行时性能损失!

Deref 强制转换如何与可变性交互

类似于您使用 Deref 特性来重载不可变引用上的 * 操作符,您可以使用 DerefMut 特性来重载可变引用上的 * 操作符。

Rust 在三种情况下发现类型和特征实现时会进行解引用强制:

  • &T&UT: Deref<Target=U>
  • &mut T&mut UT: DerefMut<Target=U>
  • &mut T&UT: Deref<Target=U>

前两个案例彼此相同,只是第二个实现了可变性。第一个案例说明,如果你有一个&T,并且T实现了到某种类型UDeref,你可以透明地获得一个&U。第二个案例说明,对于可变引用,同样的解引用强制也会发生。

第三种情况更复杂:Rust 还会将可变引用强制转换为不可变引用。但反向转换是不可能的:不可变引用永远不会强制转换为可变引用。由于借用规则,如果你有一个可变引用,那么这个可变引用必须是该数据的唯一引用(否则,程序将无法编译)。将一个可变引用转换为一个不可变引用永远不会违反借用规则。将不可变引用转换为可变引用需要初始不可变引用是该数据的唯一不可变引用,但借用规则并不能保证这一点。因此,Rust 不能假设将不可变引用转换为可变引用是可能的。