1.介绍
方法调用的点操作符看起来简单,实际上非常不简单,它在调用时,会发生很多魔法般的类型转换,例如:自动引用
、自动解引用
,强制类型转换直到类型能匹配
等。
假设有一个方法 foo,它有一个接收器(接收器就是 self
、&self
、&mut self
参数)。如果调用 value.foo(),编译器在调用 foo 之前,需要决定到底使用哪个 Self 类型来调用。现在假设 value 拥有类型 T。再进一步,我们使用完全限定语法来进行准确的函数调用。
// 完全限定语法
<Dog as Animal>::baby_name();
- 1.首先,编译器检查它是否可以直接调用 T::foo(value),称之为值方法调用.
- 2.如果上一步调用无法完成(例如方法类型错误或者特征没有针对 Self 进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用: <&T>::foo(value) 和 <&mut T>::foo(value),称之为引用方法调用
- 3.若上面两个方法依然不工作,编译器会试着解引用 T ,然后再进行尝试。
- 这里使用了 Deref 特征 —— 若 T: Deref<Target = U> (T 可以被解引用为 U),那么编译器会使用 U 类型进行尝试,称之为解引用方法调用。
- 4.若 T 不能被解引用,且 T 是一个定长类型(在编译器类型长度是已知的),那么编译器也会尝试将T从定长类型转为不定长类型,例如将 [i32; 2] 转为 [i32]
- 5.若还是不行,那...没有那了,最后编译器大喊一声:汝欺我甚,不干了!
2.例子
2.1
下面我们来用一个例子来解释上面的方法查找算法:
use std::rc::Rc;
fn main() {
let array: Rc<Box<[i32; 3]>> = Rc::new(Box::new([1, 2, 3]));
println!("{}", array[0]);
}
// 结果:1
array 数组的底层数据隐藏在了重重封锁之后,那么编译器如何使用 array[0] 这种数组原生访问语法通过重重封锁,准确的访问到数组中的第一个元素?
- 1.首先, array[0] 只是Index特征的语法糖:编译器会将 array[0] 转换为 array.index(0) 调用,当然在调用之前,编译器会先检查 array 是否实现了 Index 特征。
- 2.接着,编译器检查
Rc<Box<[T; 3]>>
是否有实现 Index 特征,结果是否,不仅如此,&Rc<Box<[T; 3]>>
与&mut Rc<Box<[T; 3]>>
也没有实现。 - 3.上面的都不能工作,编译器开始对
Rc<Box<[T; 3]>>
进行解引用,把它转变成Box<[T; 3]>
- 4.此时继续对
Box<[T; 3]>
进行上面的操作:Box<[T; 3]>
,&Box<[T; 3]>
,和&mut Box<[T; 3]>
都没有实现 Index 特征,所以编译器开始对Box<[T; 3]>
进行解引用,然后我们得到了[T; 3]
- 5.[T; 3] 以及它的各种引用都没有实现 Index 索引(是不是很反直觉:D,在直觉中,数组都可以通过索引访问,实际上只有数组切片才可以!),它也不能再进行解引用,因此编译器只能祭出最后的大杀器:
- 将定长转为不定长,因此 [T; 3] 被转换成 [T],也就是数组切片,它实现了 Index 特征,因此最终我们可以通过 index 方法访问到对应的元素。
2.2
再来看看以下更复杂的例子:
fn do_stuff<T: Clone>(value: &T) {
let cloned = value.clone();
}
上面例子中 cloned 的类型是什么?首先编译器检查能不能进行值方法调用, value 的类型是 &T,同时 clone 方法的签名也是 &T : fn clone(&T) -> T
,因此可以进行值方法调用,再加上编译器知道了 T 实现了 Clone,因此 cloned 的类型是 T。
如果 T: Clone 的特征约束被移除呢?
fn do_stuff<T>(value: &T) {
let cloned = value.clone();
}
首先,从直觉上来说,该方法会报错,因为 T 没有实现 Clone 特征,但是真实情况是什么呢?
我们先来推导一番。 首先通过值方法调用就不再可行,因为 T 没有实现 Clone 特征,也就无法调用 T 的 clone 方法。接着编译器尝试引用方法调用,此时 T 变成 &T,在这种情况下, clone 方法的签名如下: fn clone(&&T) -> &T,接着我们现在对 value 进行了引用。 编译器发现 &T 实现了 Clone 类型(所有的引用类型都可以被复制,因为其实就是复制一份地址),因此可以推出 cloned 也是 &T 类型。
最终,我们复制出一份引用指针,这很合理,因为值类型 T 没有实现 Clone,只能去复制一个指针了。
2.3
下面的例子也是自动引用生效的地方:
#[derive(Clone)]
struct Container<T>(Arc<T>);
fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) {
let foo_cloned = foo.clone();
let bar_cloned = bar.clone();
}
推断下上面的 foo_cloned 和 bar_cloned 是什么类型?提示: 关键在 Container 的泛型参数,一个是 i32 的具体类型,一个是泛型类型,其中 i32 实现了 Clone,但是 T 并没有。
首先要复习一下复杂类型派生 Clone 的规则:
- 一个复杂类型能否派生 Clone,需要它内部的所有子类型都能进行Clone。
- 因此 Container
(Arc ) 是否实现 Clone 的关键在于 T 类型是否实现了 Clone 特征。
上面代码中,Containerfn clone(&T) -> T
,由此可以看出 foo_cloned 的类型是Container
然而,bar_cloned 的类型却是&Container<T>
,这个不合理啊,明明我们为Container
impl<T> Clone for Container<T> where T: Clone {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
从上面代码可以看出,派生 Clone 能实现的根本是 T 实现了Clone特征:where T: Clone, 因此 Container
编译器接着会去尝试引用方法调用,此时 &Container
当然,也可以为 Container
impl<T> Clone for Container<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
此时,编译器首次尝试值方法调用即可通过,因此 bar_cloned 的类型变成 Container