将智能指针视为具有 Deref 特征的常规引用

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

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

注意:我们将要要使用的类型之间有一个很大的区别 build 和 real :我们的版本不会将其数据存储在堆上。 我们将此示例的重点放在 上,因此数据的实际存储位置 不如指针式行为重要。MyBox<T>Box<T>Deref

将指针指向值

常规引用是一种指针,指针的一种理解方式是 作为箭头,指向存储在其他位置的 Value。在示例 15-6 中,我们创建了一个 引用一个值,然后使用 dereference 运算符跟在 对值的引用:i32

文件名: src/main.rs

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

示例 15-6:使用 dereference 运算符跟随 对 i32 值的引用

该变量保存一个值 。我们将 等于 的引用设置为 。我们可以断言 等于 。但是,如果我们想让 断言 中的 值,我们必须使用 来遵循引用 )的值(因此取消引用),以便编译器可以比较 实际值。一旦我们取消引用 ,我们就可以访问指向的整数值,我们可以将其与 进行比较。xi325yxx5y*yyy5

如果我们尝试编写,我们将得到这个编译 错误: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 中的代码,使用 a 而不是 参考;示例 15-7 中 the 上使用的 dereference 运算符 函数的运行方式与在 示例 15-6:Box<T>Box<T>

文件名: 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 上使用 dereference 运算符>

示例 15-7 和示例 15-6 之间的主要区别在于,这里我们设置为一个指向 rather 的复制值的实例 比指向 的值的引用 .在最后一个断言中,我们可以 使用 dereference 运算符跟随 in the same 的指针 我们所做的方式 when 是一个参考。接下来,我们将探讨什么是特别的 about 这使我们能够通过定义 own 类型。yBox<T>xxBox<T>yBox<T>

定义我们自己的智能指针

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

该类型最终定义为具有一个元素的元组结构体,因此 示例 15-8 以相同的方式定义类型。我们还将定义一个函数来匹配 上定义的函数。Box<T>MyBox<T>newnewBox<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> 类型

我们定义了一个名为 struct 并声明了一个泛型参数 ,因为 我们希望我们的类型包含任何类型的值。类型是元组结构 具有一个类型为 .该函数采用 type 并返回一个包含传入值的实例。MyBoxTMyBoxTMyBox::newTMyBox

让我们尝试将示例 15-7 中的函数添加到示例 15-8 中,然后 将其更改为使用我们定义的类型,而不是 .这 示例 15-9 中的代码无法编译,因为 Rust 不知道如何取消引用 。mainMyBox<T>Box<T>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> 我们使用 references 和 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

我们的类型不能被取消引用,因为我们还没有实现它 能力在我们的类型上。要启用运算符的取消引用,我们 实现 trait。MyBox<T>*Deref

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

如第 10 章的 “在类型上实现 trait” 部分所讨论的,要实现 trait,我们需要提供 trait 的 required methods的实现。trait 中提供的 通过标准库,要求我们实现一个名为 借用并返回对内部数据的引用。示例 15-10 包含 to add 的实现 :DerefderefselfDerefMyBox

文件名: 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>

该语法定义了要使用的特征的关联类型。关联类型是声明 generic 参数,但您现在无需担心它们;我们将涵盖 它们在第 19 章中有更详细的解释。type Target = T;Deref

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

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

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

*(y.deref())

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

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

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

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

Deref 强制转换将对实现该 trait 的类型的引用转换为对另一种类型的引用。例如,deref coercion can convert to because 实现了 trait,使其 返回。Deref 强制转换是 Rust 对参数执行的一种便利 函数和方法,并且仅适用于实现 trait 的类型。当我们将引用传递给特定类型的 value 作为与参数不匹配的函数或方法的参数 键入函数或方法定义。对该方法的一系列调用将我们提供的类型转换为参数所需的类型。Deref&String&strStringDeref&strDerefderef

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

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

文件名: src/main.rs

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

fn main() {}

示例 15-11:参数名称&str 类型的 hello 函数

我们可以用字符串 slice 作为参数来调用函数,例如。Deref 强制使得使用对 type 为 的值的引用进行调用成为可能,如示例 15-12 所示:hellohello("Rust");helloMyBox<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-12:使用对 MyBox<String> 值的引用调用 hello,由于解引用强制转换,它有效

这里我们调用带有参数 的函数,它是一个 对值的引用。因为我们实现了 trait 在示例 15-10 中,Rust 可以通过调用 .标准库提供了返回字符串 slice 的 on 的实现,这在 API 文档中 为。Rust 再次调用以将 转换为 ,该 匹配函数的定义。hello&mMyBox<String>DerefMyBox<T>&MyBox<String>&StringderefDerefStringDerefderef&String&strhello

如果 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 强制

将 取消引用 为 .然后 and 取 the 的 字符串切片 that等于整个字符串 匹配 的签名。这段没有 deref 强制转换的代码更难 阅读、写入和理解所有这些符号。Deref 强制 允许 Rust 自动为我们处理这些转换。(*m)MyBox<String>String&[..]Stringhello

当为所涉及的类型定义 trait 时,Rust 将分析 类型,并根据需要多次使用以获取对 match 参数的类型。需要的次数 inserted 在编译时解析,因此采用 Deref 强制的优势!DerefDeref::derefDeref::deref

Deref Coercion 如何与可变互

类似于使用 trait 覆盖运算符 on immutable 引用,你可以使用 trait 来覆盖可变引用上的 operator。Deref*DerefMut*

Rust 在三个 例:

  • 从到时间&T&UT: Deref<Target=U>
  • 从到时间&mut T&mut UT: DerefMut<Target=U>
  • 从到时间&mut T&UT: Deref<Target=U>

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

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

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