Rust 学习笔记:Deref trait
Rust 学习笔记:Deref trait
实现 Deref trait 允许自定义解引用操作符 * 的行为。
通过实现 Deref trait,智能指针可以像普通引用一样对待。
跟随指针找到值
常规引用是指针的一种类型,将指针理解为指向存储在其他地方的值的箭头。
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
变量 x 的值是 i32。设 y 等于对 x 的一个引用,可以断言 x 等于 5。然而,如果我们想要对 y 中的值做出断言,我们必须使用 *y 来对 y 解引用,就可以访问 y 所指向的整数值,将其与 5 进行比较。
如果将 y 和 5 进行比较,会报错:不允许比较数字和对数字的引用,因为它们是不同的类型。
像引用一样使用 Box<T>
我们可以重写上一节中的代码,使用 Box<T> 代替引用。
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
对 Box<T> 使用的解引用操作符与对引用使用的解引用操作符的作用相同。
两段代码的主要区别在于,这里我们将 y 设置为指向 x 拷贝值的 box 的实例,而不是指向 x 值的引用。
接下来,我们将探讨 Box<T> 的特殊之处,它使我们能够通过定义自己的类型来使用解引用操作符。
定义自己的智能指针
让我们构建一个类似于 Box<T> 的智能指针,以体验默认情况下智能指针与引用的不同行为。然后,我们将研究如何添加使用解引用操作符的功能。
Box<T> 类型最终被定义为具有一个元素的元组结构体,以同样的方式定义了 MyBox<T> 类型。我们还将定义一个 new 函数来匹配在 Box<T> 上定义的 new 函数。
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 类型元素的元组结构体。MyBox::new 函数接受一个 T 类型的形参,并返回一个 MyBox 实例,该实例保存传入的值。
让我们尝试使用 MyBox 类型,运行上面的代码,结果如下:
我们的 MyBox<T> 类型不能被解引用,因为我们还没有在我们的类型上实现 Deref trait。
实现 Deref trait
为了实现 trait,我们需要为 trait 所需的方法提供实现。
标准库提供的 Deref trait 要求我们实现一个名为 Deref 的方法,该方法借用 self 并返回对内部数据的引用。
让我们为 MyBox 类型实现 Deref 方法:
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
type Target = T;
语法为 Deref trait 定义了要使用的关联类型。关联类型是声明泛型参数的一种稍微不同的方式。
我们用 &self 填充 deref 方法的主体。因此,deref 返回对要用 * 操作符访问的值的引用,.0 访问元组结构中的第一个值。
没有 Deref trait,编译器只能解引用 & 引用。deref 方法使编译器能够接受实现 Deref trait 的任何类型的值,并调用 deref 方法来获取一个它知道如何解引用的 & 引用。
当我们使用 *y 时,Rust 在幕后实际运行了这段代码:
*(y.deref())
Rust 将 * 操作符替换为对 deref 方法的调用,然后转变为一个普通的解引用。这个 Rust 特性允许我们编写功能相同的代码(都使用 * 解引用),无论我们使用的是常规引用还是实现 Deref trait 的类型。
deref 方法返回一个值的引用,而在 *(y.deref())
中括号外的普通解引用仍然是必要的,这与所有权系统有关。如果 deref 方法直接返回值而不是对该值的引用,则该值将被移出 self。在这种情况下,或者在大多数使用解引用操作符的情况下,我们不希望获得 MyBox<T> 内部值的所有权。
函数和方法的隐式强制转换
Deref 强制转换将对实现 Deref trait 的类型的引用转换为对另一类型的引用。
例如,Deref 强制转换可以将 &String 转换为 &str,因为 String 实现了 Deref trait,因此它返回 &str。
Deref 强制转换是 Rust 对函数和方法的参数执行的一种便利,并且只对实现了 Deref trait 的类型起作用。当将对特定类型值的引用作为实参传递给与函数或方法定义中的形参类型不匹配的函数或方法时,会自动发生这种情况。对d eref 方法的一系列调用将我们提供的类型转换为参数所需的类型。
在 Rust 中添加了强制转换,这样编写函数和方法调用的程序员就不需要添加那么多带有 & 和 * 的显式引用和解引用。该特性还允许我们编写更多可以用于引用或智能指针的代码。
例如,我们实现了一个 hello 函数,入参 name 是 &str:
fn hello(name: &str) {
println!("Hello, {name}!");
}
Deref 强制转换使得使用对 MyBox<String> 类型值的引用来调用 hello 函数成为可能。
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
这里我们用参数 &m 调用 hello 函数,这是对 MyBox<String> 值的引用。因为我们对 MyBox<T> 实现了 Deref trait, Rust 可以通过调用 deref 方法将 &MyBox<String> 转换为 &String。标准库提供了一个 Deref on String 的实现,它返回一个字符串切片。Rust 再次调用 deref 将 &String 转换为 &str,这与 hello 函数的定义相匹配。
// 转换过程
&MyBox<String> -> &String -> &str
如果 Rust 没有 Deref 强制转换,我们将不得不编写这样的代码,以 &MyBox<String> 类型的值调用 hello 函数。
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m) 将 MyBox<String> 解引用为 String,[…] 取整个 String 为字符串切片,& 再次取引用,以匹配 hello 函数的签名。
由于涉及到所有这些符号,这段没有 Deref 强制转换的代码更难读、写和理解。强制转换允许 Rust 自动为我们处理这些转换。
当为所涉及的类型定义了 Deref trait 时,Rust 将分析这些类型,并根据需要多次使用 Deref::deref 来获取与参数类型匹配的引用。需要插入 Deref::deref 的次数是在编译时解决的,所以利用 Deref 强制转换不会造成运行时的损失!
强制转换如何与可变性相互作用
与使用 Deref trait 在不可变引用上重写 * 操作符类似,可以使用 DerefMut trait 在可变引用上重写 * 操作符。
在三种情况下,Rust 在发现类型和 trait 实现时进行强制转换:
- 当
T: Deref<Target=U>
时,从&T
到&U
- 当
T: DerefMut<Target=U>
时,从&mut T
到&mut U
- 当
T: Deref<Target=U>
时,从&mut T
到&U
前两种情况是相同的,只是第二种实现了可变性。第一种情况表明,如果你有一个 &T,并且 T 实现了对某种类型 U 的 Deref,你可以透明地获得一个 &U。第二种情况表明,对于可变引用也会发生相同的强制解引用。
第三种情况更棘手:Rust 还会将一个可变引用强制转换为一个不可变引用。但反过来是不可能的,不可变引用永远不会强制到可变引用。
由于借用规则,如果您有一个可变引用,那么该可变引用必须是对该数据的唯一引用(否则,程序将无法编译)。将一个可变引用转换为一个不可变引用永远不会违反借用规则。将不可变引用转换为可变引用将要求初始不可变引用是对该数据的唯一不可变引用,但借用规则不能保证这一点。因此,Rust 不能假设将不可变引用转换为可变引用是可能的。