将智能指针视为常规引用Deref特性

实施Dereftrait 允许您自定义 dereference 运算符的行为(不要与 multiplication 或 glob 混淆 运算符)。通过实施*Deref这样,智能指针就可以 被视为常规引用,您可以编写对 引用,并将该代码与智能指针一起使用。

首先,让我们看看 dereference 运算符如何处理常规引用。 然后我们将尝试定义一个行为类似于Box<T>,看看为什么 dereference 运算符的工作方式与我们新定义的 类型。我们将探讨如何实现Dereftrait 使 Smart Pointers 的工作方式类似于 References。然后我们来看看 Rust 的 deref 强制功能以及它如何让我们使用任一引用 或智能指针。

注意: 这两者之间有一个很大的区别MyBox<T>type 我们即将 build 和真实的Box<T>:我们的版本不会将其数据存储在堆上。 我们将此示例重点介绍Deref,即数据的实际存储位置 不如指针式行为重要。

将指针指向值

常规引用是一种指针,指针的一种理解方式是 作为箭头,指向存储在其他位置的 Value。在示例 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,我们可以访问 integer 值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>Like a Reference

我们可以重写示例 15-6 中的代码,使用Box<T>而不是 参考;在Box<T>在示例 15-7 中 函数的运行方式与在 示例 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:在Box<i32>

示例 15-7 和示例 15-6 的主要区别在于,这里我们设置了y作为Box<T>指向复制的值x而 而不是指向x.在最后一个断言中,我们可以 使用 dereference 运算符跟随Box<T>在同一 我们什么时候做的y是一个参考。接下来,我们将探讨什么是特别的 大约Box<T>这使我们能够通过定义我们的 own 类型。

定义我们自己的智能指针

让我们构建一个类似于Box<T>type 由 标准库来体验智能指针的行为与 引用。然后,我们将了解如何添加使用 dereference 运算符。

Box<T>type 最终定义为具有一个元素的元组结构,因此 示例 15-8 定义了一个MyBox<T>键入。我们还将定义一个new函数来匹配new函数定义于Box<T>.

文件名: 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因为 我们希望我们的类型包含任何类型的值。这MyBoxtype 是一个元组结构 具有一个类型为T.这MyBox::newfunction 采用 类型T并返回一个MyBox实例,该实例保存传入的值。

让我们尝试添加main示例 15-7 中的函数到示例 15-8 和 将其更改为使用MyBox<T>type 而不是Box<T>.这 示例 15-9 中的代码无法编译,因为 Rust 不知道如何取消引用MyBox.

文件名: 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:尝试使用MyBox<T>在同一 我们使用引用和Box<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>type 不能被取消引用,因为我们还没有实现它 能力在我们的类型上。要启用运算符的取消引用,我们 实现*Deref特性。

通过实现Deref特性

如第 10 章的 “在类型上实现 trait” 部分所讨论的,要实现 trait,我们需要提供 trait 的 required methods的实现。这Dereftrait 中,提供 ,要求我们实现一个名为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:实现DerefMyBox<T>

type Target = T;syntax 为Dereftrait 一起使用。关联类型是声明 generic 参数,但您现在无需担心它们;我们将涵盖 它们在第 19 章中有更详细的解释。

我们填写derefmethod 替换为&self.0所以deref返回 引用我们要使用运算符访问的值;回想一下“使用不带命名字段的元组结构创建不同的 Types“部分,其中*.0访问 元组结构中的第一个值。这main示例 15-9 中的 调用*MyBox<T>value 现在编译,并且断言通过!

如果没有Dereftrait 时,编译器只能取消引用。 这&derefmethod 使编译器能够采用任何类型的值 实现Deref并调用deref方法获取引用,该引用 它知道如何取消引用。&

当我们进入*y在示例 15-9 中,Rust 在幕后实际上运行了这个 法典:

*(y.deref())

Rust 将运算符替换为对*deref方法,然后是 plain dereference,因此我们不必考虑是否需要 调用deref方法。这个 Rust 功能让我们编写代码,将 无论我们有一个常规的引用还是一个实现Deref.

原因deref方法返回对值的引用,并且 括号外的普通取消引用*(y.deref())仍然是必需的, 与所有权制度有关。如果derefmethod 返回值 而不是对值的引用,而是将该值从self.我们不想拥有内在价值的所有权MyBox<T>在 在这种情况下,或者在大多数情况下,我们使用 dereference 运算符。

请注意,运算符将替换为对*derefmethod 和 然后只调用一次运算符,每次我们在代码中使用 a 时。 因为运算符的替换不是无限递归的,所以我们 最终得到 data 类型的***i32,它与5assert_eq!在 示例 15-9.

使用函数和方法的隐式 Deref 强制转换

Deref 强制将引用转换为实现Dereftrait 转换为对另一种类型的引用。例如,deref 强制转换&String&str因为String实现Dereftrait 的 Trait 中,它 返回&str.Deref 强制转换是 Rust 对参数执行的一种便利 函数和方法,并且仅适用于实现Deref特性。当我们将引用传递给特定类型的 value 作为与参数不匹配的函数或方法的参数 键入函数或方法定义。对derefmethod 将我们提供的类型转换为参数所需的类型。

Deref 强制转换被添加到 Rust 中,以便程序员编写函数和 方法调用不需要添加尽可能多的显式引用和取消引用 with 和 .deref 强制功能还允许我们编写更多代码 可以用于引用或智能指针。&*

要查看 deref 强制转换的实际效果,让我们使用MyBox<T>type 示例 15-8 以及Deref我们在 清单 中添加的 15-10. 示例 15-11 显示了具有字符串 slice 的函数的定义 参数:

文件名: src/main.rs

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}

示例 15-11:一个hello函数,该函数的参数为name的类型&str

我们可以调用hello函数,例如hello("Rust");例如。Deref 强制使得调用hello引用 type 为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:调用hello引用MyBox<String>值,由于 deref 强制转换而有效

这里我们调用hello函数,参数为&m,它是一个 对MyBox<String>价值。因为我们实现了Deref特性 上MyBox<T>在示例 15-10 中,Rust 可以&MyBox<String>&String通过调用deref.标准库提供了DerefString返回一个字符串 slice,这在 API 文档中 为Deref.Rust 调用deref再次转动&String&str哪 匹配hello函数的定义。

如果 Rust 没有实现 deref 强制转换,我们将不得不将代码写入 示例 15-13 而不是示例 15-12 中的代码来调用hello具有值 的类型&MyBox<String>.

文件名: 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等于整个字符串 匹配hello.这段没有 deref 强制转换的代码更难 阅读、写入和理解所有这些符号。Deref 强制 允许 Rust 自动为我们处理这些转换。

Dereftrait 定义时,Rust 将分析 类型和用途Deref::deref根据需要多次获取对 match 参数的类型。该Deref::deref需要 inserted 在编译时解析,因此采用 Deref 强制的优势!

Deref Coercion 如何与可变互

类似于您使用Dereftrait 覆盖 operator on immutable 引用,您可以使用*DerefMuttrait 覆盖可变引用上的运算符。*

Rust 在三个 例:

  • &T&U什么时候T: Deref<Target=U>
  • &mut T&mut U什么时候T: DerefMut<Target=U>
  • &mut T&U什么时候T: Deref<Target=U>

前两种情况彼此相同,只是第二种情况 实现可变性。第一种情况表明,如果你有一个&TT实现Deref到某种类型U,您可以获得&U透明。这 第二种情况表明,可变引用会发生相同的 deref 强制转换。

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

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准