以下学习来自 Rust 语言圣经和 Rust 程序设计语言
1.方法调用内置规则
假设 value 为 T 类型,调用 value.foo()
,编译器会做些:
- 首先,编译器检查它是否可以直接调用
T::foo(value)
,称之为值方法调用 - 如果上一步调用无法完成(例如方法类型错误或者特征没有针对
Self
进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用:<&T>::foo(value)
和<&mut T>::foo(value)
,称之为引用方法调用 - 若上面两个方法依然不工作,编译器会试着解引用
T
,然后再进行尝试。这里使用了Deref
特征 —— 若T: Deref<Target = U>
(T
可以被解引用为U
),那么编译器会使用U
类型进行尝试,称之为解引用方法调用 - 若
T
不能被解引用,且T
是一个定长类型(在编译期类型长度是已知的),那么编译器也会尝试将T
从定长类型转为不定长类型,例如将[i32; 2]
转为[i32]
- 若还是不行,那…没有那了,最后编译器大喊一声:汝欺我甚,不干了!
下面我们来用一个例子来解释上面的方法查找算法:
let array: Rc<Box<[T; 3]>> = ...;
let first_entry = array[0];
array
数组的底层数据套壳在 RC、BOX 等类型中,那么编译器如何使用 array[0]
这种数组原生访问语法通过重重封锁,准确的访问到数组中的第一个元素?
- 首先,
array[0]
只是Index
特征的语法糖:编译器会将array[0]
转换为array.index(0)
调用,当然在调用之前,编译器会先检查array
是否实现了Index
特征。 - 接着,编译器检查
Rc<Box<[T; 3]>>
是否有实现Index
特征,结果是否,不仅如此,&Rc<Box<[T; 3]>>
与&mut Rc<Box<[T; 3]>>
也没有实现。 - 上面的都不能工作,编译器开始对
Rc<Box<[T; 3]>>
进行解引用,把它转变成Box<[T; 3]>
- 此时继续对
Box<[T; 3]>
进行上面的操作 :Box<[T; 3]>
,&Box<[T; 3]>
,和&mut Box<[T; 3]>
都没有实现Index
特征,所以编译器开始对Box<[T; 3]>
进行解引用,然后我们得到了[T; 3]
[T; 3]
以及它的各种引用都没有实现Index
索引(是不是很反直觉:D,在直觉中,数组都可以通过索引访问,实际上只有数组切片才可以!),它也不能再进行解引用,因此编译器只能祭出最后的大杀器:将定长转为不定长,因此[T; 3]
被转换成[T]
,也就是数组切片,它实现了Index
特征,因此最终我们可以通过index
方法访问到对应的元素。
通过上面的例子看到解壳的过程就是进行解引用进入到内层类型进行查找,这样持续递归,这样我们就能达到调用被套壳对象的方法了。
2.隐式 Deref 拆解
Deref 强制转换是 Rust 在函数或方法传参上的一种便利操作,只能作用于实现了 Deref
trait 的类型。通过上面例子也可以看到隐式 Deref 将原始的类型转换成了一堆其他类型进行尝试方法的调用。
Rc<Box<[T; 3]>> =》 Box<[T; 3]> =》[T; 3]
- 自定义类型
MyBox<T>
上实现Deref
use std::ops::Deref;
impl<T> Deref for MyBox<T> {type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
- 使用
MyBox
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
这里使用 &m
调用 hello
函数,其为 MyBox<String>
值的引用.
解隐用过程:
- 编译器会自动尝试调用 m 的 deref 方法,在
MyBox<T>
上实现了Deref
trait,调用deref
方法后将&MyBox<String>
变为&String
,即&m.deref() - 第一次 deref 后发现类型还是没法匹配,编译器继续尝试当前类型的 deref 方法,
String
实现了Deref
并会返回字符串 slice,这样 Rust 调用deref
方法后类型会从&String
变为&str
,这就符合hello
函数的定义了
备注:这些解析查找都发生在编译时,所以利用 Deref 强制转换并没有运行时损耗!