通过 Deref trait 将智能指针当作常规引用处理
Treating Smart Pointers Like Regular References
实现 Deref trait 允许你自定义 解引用操作符(dereference operator)* 的行为(不要将其与乘法或通配符操作符混淆)。通过实现 Deref 使得智能指针可以被当作常规引用对待,你可以编写运行在引用上的代码,并将其同样用于智能指针。
Implementing the Deref trait allows you to customize the behavior of the
dereference operator * (not to be confused with the multiplication or glob
operator). By implementing Deref in such a way that a smart pointer can be
treated like a regular reference, you can write code that operates on
references and use that code with smart pointers too.
让我们首先看看解引用操作符如何作用于常规引用。然后,我们将尝试定义一个行为类似于 Box<T> 的自定义类型,并看看为什么解引用操作符在我们的新定义类型上不能像引用那样工作。我们将探索实现 Deref trait 如何使智能指针以类似于引用的方式工作。接着,我们将研究 Rust 的解引用强制转换(deref coercion)功能,以及它如何让我们同时处理引用或智能指针。
Let’s first look at how the dereference operator works with regular references.
Then, we’ll try to define a custom type that behaves like Box<T> and see why
the dereference operator doesn’t work like a reference on our newly defined
type. We’ll explore how implementing the Deref trait makes it possible for
smart pointers to work in ways similar to references. Then, we’ll look at
Rust’s deref coercion feature and how it lets us work with either references or
smart pointers.
通过解引用操作符追踪指针指向的值
Following the Reference to the Value
常规引用是一种指针,可以将指针想象成指向存储在别处的值的箭头。在示例 15-6 中,我们创建了一个指向 i32 值的引用,然后使用解引用操作符来追踪该引用指向的值。
A regular reference is a type of pointer, and one way to think of a pointer is as an arrow to a value stored somewhere else. In Listing 15-6, we create a reference to an i32 value and then use the dereference operator to follow the reference to the value.
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
变量 x 持有一个 i32 值 5。我们将 y 设置为 x 的引用。我们可以断言 x 等于 5。然而,如果我们想对 y 中的值进行断言,我们必须使用 *y 来追踪引用指向的值(即 解引用),以便编译器可以比较实际的值。一旦我们对 y 进行了解引用,我们就可以访问 y 指向的整数值,并将其与 5 进行比较。
The variable x holds an i32 value 5. We set y equal to a reference to
x. We can assert that x is equal to 5. However, if we want to make an
assertion about the value in y, we have to use *y to follow the reference
to the value it’s pointing to (hence, dereference) so that the compiler can
compare the actual value. Once we dereference y, we have access to the
integer value y is pointing to that we can compare with 5.
如果我们尝试写 assert_eq!(5, y);,我们会得到如下编译错误:
If we tried to write assert_eq!(5, y); instead, we would get this compilation
error:
$ 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
比较数字和数字的引用是不允许的,因为它们是不同的类型。我们必须使用解引用操作符来追踪引用指向的值。
Comparing a number and a reference to a number isn’t allowed because they’re different types. We must use the dereference operator to follow the reference to the value it’s pointing to.
像使用引用一样使用 Box<T>
Using Box<T> Like a Reference
我们可以重写示例 15-6 中的代码,使用 Box<T> 代替引用;在示例 15-7 中作用于 Box<T> 的解引用操作符的功能,与示例 15-6 中作用于引用的解引用操作符相同。
We can rewrite the code in Listing 15-6 to use a Box<T> instead of a
reference; the dereference operator used on the Box<T> in Listing 15-7
functions in the same way as the dereference operator used on the reference in
Listing 15-6.
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
示例 15-7 与示例 15-6 的主要区别在于,这里我们将 y 设置为一个指向 x 值副本的 box 实例,而不是一个指向 x 值的引用。在最后的断言中,我们可以使用解引用操作符来追踪 box 的指针,就像 y 是引用时那样。接下来,我们将通过定义我们自己的 box 类型来探索 Box<T> 的特殊之处,正是这种特殊性使我们能够使用解引用操作符。
The main difference between Listing 15-7 and Listing 15-6 is that here we set
y to be an instance of a box pointing to a copied value of x rather than a
reference pointing to the value of x. In the last assertion, we can use the
dereference operator to follow the box’s pointer in the same way that we did
when y was a reference. Next, we’ll explore what is special about Box<T>
that enables us to use the dereference operator by defining our own box type.
自定义智能指针
Defining Our Own Smart Pointer
让我们构建一个类似于标准库提供的 Box<T> 类型的封装类型,以体验智能指针类型在默认情况下与引用的不同行为。然后,我们将研究如何添加使用解引用操作符的能力。
Let’s build a wrapper type similar to the Box<T> type provided by the
standard library to experience how smart pointer types behave differently from
references by default. Then, we’ll look at how to add the ability to use the
dereference operator.
注意:我们要构建的
MyBox<T>类型与真实的Box<T>之间有一个很大的区别:我们的版本不会将其数据存储在堆上。本例重点关注Deref,所以数据实际存储在哪里并不如类似指针的行为那么重要。
Note: There’s one big difference between the
MyBox<T>type we’re about to build and the realBox<T>: Our version will not store its data on the heap. We are focusing this example onDeref, so where the data is actually stored is less important than the pointer-like behavior.
Box<T> 类型最终被定义为带有一个元素的元组结构体,因此示例 15-8 以相同的方式定义了一个 MyBox<T> 类型。我们还将定义一个 new 函数来匹配 Box<T> 上定义的 new 函数。
The Box<T> type is ultimately defined as a tuple struct with one element, so
Listing 15-8 defines a MyBox<T> type in the same way. We’ll also define a
new function to match the new function defined on Box<T>.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
我们定义了一个名为 MyBox 的结构体并声明了一个泛型参数 T,因为我们希望我们的类型可以持有任何类型的值。MyBox 类型是一个带有一个 T 类型元素的元组结构体。MyBox::new 函数接收一个 T 类型的参数,并返回一个持有该传入值的 MyBox 实例。
We define a struct named MyBox and declare a generic parameter T because we
want our type to hold values of any type. The MyBox type is a tuple struct
with one element of type T. The MyBox::new function takes one parameter of
type T and returns a MyBox instance that holds the value passed in.
让我们尝试将示例 15-7 中的 main 函数添加到示例 15-8 中,并将其改为使用我们定义的 MyBox<T> 类型而不是 Box<T>。示例 15-9 中的代码还无法编译,因为 Rust 不知道如何解引用 MyBox。
Let’s try adding the main function in Listing 15-7 to Listing 15-8 and
changing it to use the MyBox<T> type we’ve defined instead of Box<T>. The
code in Listing 15-9 won’t compile, because Rust doesn’t know how to
dereference 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);
}
这是生成的编译错误:
Here’s the resultant 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);
| ^^ 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。
Our MyBox<T> type can’t be dereferenced because we haven’t implemented that
ability on our type. To enable dereferencing with the * operator, we
implement the Deref trait.
实现 Deref trait
Implementing the Deref Trait
正如第 10 章“为类型实现 Trait”中讨论的,要实现一个 trait,我们需要提供该 trait 所需方法的实现。由标准库提供的 Deref trait 要求我们实现一个名为 deref 的方法,该方法借用 self 并返回一个指向内部数据的引用。示例 15-10 包含了一个添加到 MyBox<T> 定义中的 Deref 实现。
As discussed in “Implementing a Trait on a Type” in
Chapter 10, to implement a trait we need to provide implementations for the
trait’s required methods. The Deref trait, provided by the standard library,
requires us to implement one method named deref that borrows self and
returns a reference to the inner data. Listing 15-10 contains an implementation
of Deref to add to the definition of MyBox<T>.
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);
}
type Target = T; 语法为 Deref trait 定义了一个供其使用的关联类型。关联类型是声明泛型参数的一种略有不同的方式,但现在你不需要担心它们;我们将在第 20 章详细讨论。
The type Target = T; syntax defines an associated type for the Deref trait
to use. Associated types are a slightly different way of declaring a generic
parameter, but you don’t need to worry about them for now; we’ll cover them in
more detail in Chapter 20.
我们在 deref 方法体中填充了 &self.0,这样 deref 就会返回一个指向我们想要通过 * 操作符访问的值的引用;回想一下第 5 章“使用元组结构体创建不同类型”,.0 用来访问元组结构体中的第一个值。示例 15-9 中对 MyBox<T> 值调用 * 的 main 函数现在可以编译了,并且断言也能通过了!
We fill in the body of the deref method with &self.0 so that deref
returns a reference to the value we want to access with the * operator;
recall from “Creating Different Types with Tuple Structs” in Chapter 5 that .0 accesses the first value in a tuple struct.
The main function in Listing 15-9 that calls * on the MyBox<T> value now
compiles, and the assertions pass!
如果没有 Deref trait,编译器只能解引用 & 引用。deref 方法赋予了编译器这样一种能力:获取任何实现了 Deref 的类型的值,并调用 deref 方法以获得一个它知道如何解引用的引用。
Without the Deref trait, the compiler can only dereference & references.
The deref method gives the compiler the ability to take a value of any type
that implements Deref and call the deref method to get a reference that
it knows how to dereference.
当我们在示例 15-9 中输入 *y 时,在幕后 Rust 实际上运行了这段代码:
When we entered *y in Listing 15-9, behind the scenes Rust actually ran this
code:
*(y.deref())
Rust 将 * 操作符替换为对 deref 方法的调用,然后再进行一次普通解引用,这样我们就不必考虑是否需要调用 deref 方法。Rust 的这一特性让我们编写的代码无论是在使用常规引用还是实现了 Deref 的类型时,其功能都是完全相同的。
Rust substitutes the * operator with a call to the deref method and then a
plain dereference so that we don’t have to think about whether or not we need
to call the deref method. This Rust feature lets us write code that functions
identically whether we have a regular reference or a type that implements
Deref.
deref 方法返回一个指向值的引用,并且在 *(y.deref()) 括号外仍然需要进行普通解引用的原因,与所有权系统有关。如果 deref 直接返回该值而不是指向该值的引用,那么该值将从 self 中移出。在这种情况下,或者在大多数使用解引用操作符的情况下,我们都不希望获取 MyBox<T> 内部值的所有权。
The reason the deref method returns a reference to a value, and that the
plain dereference outside the parentheses in *(y.deref()) is still necessary,
has to do with the ownership system. If the deref method returned the value
directly instead of a reference to the value, the value would be moved out of
self. We don’t want to take ownership of the inner value inside MyBox<T> in
this case or in most cases where we use the dereference operator.
请注意,每当我们代码中使用一次 * 时,该 * 操作符都会被替换为一次对 deref 方法的调用和一次对 * 操作符的调用。由于对 * 操作符的替换不会无限递归,我们最终会得到 i32 类型的数据,它与示例 15-9 中 assert_eq! 里的 5 相匹配。
Note that the * operator is replaced with a call to the deref method and
then a call to the * operator just once, each time we use a * in our code.
Because the substitution of the * operator does not recurse infinitely, we
end up with data of type i32, which matches the 5 in assert_eq! in
Listing 15-9.
函数和方法中的解引用强制转换
Using Deref Coercion in Functions and Methods
解引用强制转换(Deref coercion)将一个实现了 Deref trait 的类型的引用转换为另一个类型的引用。例如,解引用强制转换可以将 &String 转换为 &str,因为 String 实现了 Deref trait 并由此返回 &str。解引用强制转换是 Rust 对函数和方法参数执行的一种便捷操作,并且它只对实现了 Deref trait 的类型有效。当我们向函数或方法传递特定类型值的引用作为参数,而该引用与函数或方法定义中的参数类型不匹配时,这种转换就会自动发生。一系列对 deref 方法的调用会将我们提供的类型转换为参数所需的类型。
Deref coercion converts a reference to a type that implements the Deref
trait into a reference to another type. For example, deref coercion can convert
&String to &str because String implements the Deref trait such that it
returns &str. Deref coercion is a convenience Rust performs on arguments to
functions and methods, and it works only on types that implement the Deref
trait. It happens automatically when we pass a reference to a particular type’s
value as an argument to a function or method that doesn’t match the parameter
type in the function or method definition. A sequence of calls to the deref
method converts the type we provided into the type the parameter needs.
解引用强制转换被加入到 Rust 中,是为了让编写函数和方法调用的程序员不必添加那么多显式的使用 & 和 * 的引用和解引用。解引用强制转换特性还让我们能编写出更多可以同时适用于引用或智能指针的代码。
Deref coercion was added to Rust so that programmers writing function and
method calls don’t need to add as many explicit references and dereferences
with & and *. The deref coercion feature also lets us write more code that
can work for either references or smart pointers.
为了观察解引用强制转换的实际应用,我们将使用示例 15-8 中定义的 MyBox<T> 类型,以及示例 15-10 中添加的 Deref 实现。示例 15-11 展示了一个带有字符串切片参数的函数定义。
To see deref coercion in action, let’s use the MyBox<T> type we defined in
Listing 15-8 as well as the implementation of Deref that we added in Listing
15-10. Listing 15-11 shows the definition of a function that has a string slice
parameter.
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
我们可以使用字符串切片作为参数来调用 hello 函数,例如 hello("Rust");。解引用强制转换使得使用 MyBox<String> 类型值的引用来调用 hello 成为可能,如示例 15-12 所示。
We can call the hello function with a string slice as an argument, such as
hello("Rust");, for example. Deref coercion makes it possible to call hello
with a reference to a value of type MyBox<String>, as shown in 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);
}
这里我们使用参数 &m 调用 hello 函数,它是指向 MyBox<String> 值的引用。由于我们在示例 15-10 中为 MyBox<T> 实现了 Deref trait,Rust 可以通过调用 deref 将 &MyBox<String> 转换为 &String。标准库为 String 提供了返回字符串切片的 Deref 实现,这可以在 Deref 的 API 文档中找到。Rust 再次调用 deref 将 &String 转换为 &str,从而匹配 hello 函数的定义。
Here we’re calling the hello function with the argument &m, which is a
reference to a MyBox<String> value. Because we implemented the Deref trait
on MyBox<T> in Listing 15-10, Rust can turn &MyBox<String> into &String
by calling deref. The standard library provides an implementation of Deref
on String that returns a string slice, and this is in the API documentation
for Deref. Rust calls deref again to turn the &String into &str, which
matches the hello function’s definition.
如果 Rust 没有实现解引用强制转换,为了使用 &MyBox<String> 类型的值调用 hello,我们必须编写示例 15-13 中的代码,而不是示例 15-12 中的代码。
If Rust didn’t implement deref coercion, we would have to write the code in
Listing 15-13 instead of the code in Listing 15-12 to call hello with a value
of type &MyBox<String>.
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 自动为我们处理这些转换。
The (*m) dereferences the MyBox<String> into a String. Then, the & and
[..] take a string slice of the String that is equal to the whole string to
match the signature of hello. This code without deref coercions is harder to
read, write, and understand with all of these symbols involved. Deref coercion
allows Rust to handle these conversions for us automatically.
当为涉及的类型定义了 Deref trait 时,Rust 将分析这些类型,并根据需要多次使用 Deref::deref 以获得匹配参数类型的引用。需要插入 Deref::deref 的次数是在编译时决定的,因此利用解引用强制转换没有运行时开销!
When the Deref trait is defined for the types involved, Rust will analyze the
types and use Deref::deref as many times as necessary to get a reference to
match the parameter’s type. The number of times that Deref::deref needs to be
inserted is resolved at compile time, so there is no runtime penalty for taking
advantage of deref coercion!
解引用强制转换如何与可变性交互
Handling Deref Coercion with Mutable References
类似于使用 Deref trait 覆盖不可变引用的 * 操作符,你可以使用 DerefMut trait 来覆盖可变引用的 * 操作符。
Similar to how you use the Deref trait to override the * operator on
immutable references, you can use the DerefMut trait to override the *
operator on mutable references.
当 Rust 发现类型和 trait 实现满足以下三种情况时,它会执行解引用强制转换:
Rust does deref coercion when it finds types and trait implementations in three cases:
-
当
T: Deref<Target=U>时,从&T到&U -
From
&Tto&UwhenT: Deref<Target=U> -
当
T: DerefMut<Target=U>时,从&mut T到&mut U -
From
&mut Tto&mut UwhenT: DerefMut<Target=U> -
当
T: Deref<Target=U>时,从&mut T到&U -
From
&mut Tto&UwhenT: Deref<Target=U>
前两种情况是相同的,除了第二种情况实现了可变性。第一种情况声明:如果你有一个 &T,且 T 为某种类型 U 实现了 Deref,你可以无缝地获得一个 &U。第二种情况声明:相同的解引用强制转换也发生在可变引用上。
The first two cases are the same except that the second implements mutability.
The first case states that if you have a &T, and T implements Deref to
some type U, you can get a &U transparently. The second case states that
the same deref coercion happens for mutable references.
第三种情况比较微妙:Rust 也会将可变引用强制转换为不可变引用。但反之则 不 可能:不可变引用永远不会强制转换为可变引用。根据借用规则,如果你有一个可变引用,那么该可变引用必须是该数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为一个不可变引用永远不会破坏借用规则。将不可变引用转换为可变引用则要求初始的不可变引用是该数据的唯一不可变引用,但借用规则并不保证这一点。因此,Rust 不能假定将不可变引用转换为可变引用是可能的。
The third case is trickier: Rust will also coerce a mutable reference to an immutable one. But the reverse is not possible: Immutable references will never coerce to mutable references. Because of the borrowing rules, if you have a mutable reference, that mutable reference must be the only reference to that data (otherwise, the program wouldn’t compile). Converting one mutable reference to one immutable reference will never break the borrowing rules. Converting an immutable reference to a mutable reference would require that the initial immutable reference is the only immutable reference to that data, but the borrowing rules don’t guarantee that. Therefore, Rust can’t make the assumption that converting an immutable reference to a mutable reference is possible.