15.3 Deref trait Pt.2:隐式解引用转化与可变性
15.3.1 函数和方法的隐式解引用转化
隐式解引用转化(deref coercion)是为函数和方法提供的一种便捷特性。
它的原理是:假如类型 T 实现了 Deref trait,那么解引用转化可以把 T 的引用转换成对 T 应用 Deref 之后得到的引用。
当某个类型的引用被传给函数或方法,但其类型与声明的参数类型不匹配时,解引用转化会自动发生。编译器会对 deref 进行一系列调用,把它转换成所需的参数类型。这个过程在编译时完成,因此没有额外的性能开销。
这句话比较抽象,看个例子就明白了。我们接着上一篇文章的代码来写:
#![allow(unused)]
fn main() {
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
}
这是上一篇文章的代码。它定义了 MyBox 元组结构体(元组结构体的介绍详见 5.1. 定义并实例化struct),创建了 new 函数,并为其实现了 Deref trait,因此可以对 MyBox 使用普通的解引用操作。
以下是新增部分:
#![allow(unused)]
fn main() {
fn hello(name: &str) {
println!("Hello, {}", name);
}
}
hello 函数接收 &str,也就是字符串切片,然后把它打印出来。
再看主函数:
fn main(){
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
m 是 MyBox<String> 类型,&m 就是 &MyBox<String>。然而 hello 期望的是 &str,但这段代码并不会报错。为什么?
首先,MyBox 已经实现了 Deref trait,所以 Rust 可以调用 deref,把 &MyBox<String> 转换成 &String。这就是刚才那个比较抽象的规则。
到这一步还没完。&String 和 &str 是不同的类型,那又是怎么转换的呢?因为 String 也实现了 Deref trait,而且它的 deref 实现返回的是 &str 类型的字符串切片,所以 Rust 会对 &String 使用 deref,把 &String 转换成 &str。最终类型就匹配了。
如果 Rust 没有解引用转化,写法会是这样:
#![allow(unused)]
fn main() {
hello(&(*m)[..]);
}
- 先使用解引用运算符
*,把m从MyBox<String>转换成String。 - 再加上引用符号
&,把String转换成&String。 - 通过切片语法
[..],可以获得String完整内容的引用,并将其值从&String转换成&str。
15.3.2 解引用与可变性
可以使用 DerefMut trait 来重载可变引用的 * 运算符。与 Deref 相比,DerefMut 多了 Mut,意思是 DerefMut 返回可变引用 &mut T,而 Deref 返回不可变引用 &T。
当类型和 trait 满足下列三种情况时,Rust 会执行解引用转化:
-
当
T: Deref<Target = U>时,&T可以转换成&U:T实现了Dereftrait,且Deref下deref的返回类型是&U,因此&T可以转换成&U。 例如,上文代码中的MyBox类型实现了Deref,其deref方法返回&T,所以&MyBox可以转换成&T。 -
当
T: DerefMut<Target = U>时,&mut T可以转换成&mut U。T实现了DerefMuttrait(DerefMut返回可变引用&mut T),且DerefMut下deref的返回类型是&mut U,因此&mut T可以转换成&mut U。 -
当
T: Deref<Target = U>时,&mut T可以转换成&U。 Rust 可以自动地把可变引用转换成不可变引用,但反过来绝对不行。把不可变引用转换成可变引用要求该引用是唯一的(借用规则中有讲,详见 4.4. 引用与借用)。