我正在学习/试验Rust,在我发现的所有优雅的语言中,有一个特点让我困惑,似乎完全不合适。
Rust在进行方法调用时会自动取消引用指针。我做了一些测试来确定确切的行为:
struct X { val: i32 }
impl std::ops::Deref for X {
type Target = i32;
fn deref(&self) -> &i32 { &self.val }
}
trait M { fn m(self); }
impl M for i32 { fn m(self) { println!("i32::m()"); } }
impl M for X { fn m(self) { println!("X::m()"); } }
impl M for &X { fn m(self) { println!("&X::m()"); } }
impl M for &&X { fn m(self) { println!("&&X::m()"); } }
impl M for &&&X { fn m(self) { println!("&&&X::m()"); } }
trait RefM { fn refm(&self); }
impl RefM for i32 { fn refm(&self) { println!("i32::refm()"); } }
impl RefM for X { fn refm(&self) { println!("X::refm()"); } }
impl RefM for &X { fn refm(&self) { println!("&X::refm()"); } }
impl RefM for &&X { fn refm(&self) { println!("&&X::refm()"); } }
impl RefM for &&&X { fn refm(&self) { println!("&&&X::refm()"); } }
struct Y { val: i32 }
impl std::ops::Deref for Y {
type Target = i32;
fn deref(&self) -> &i32 { &self.val }
}
struct Z { val: Y }
impl std::ops::Deref for Z {
type Target = Y;
fn deref(&self) -> &Y { &self.val }
}
#[derive(Clone, Copy)]
struct A;
impl M for A { fn m(self) { println!("A::m()"); } }
impl M for &&&A { fn m(self) { println!("&&&A::m()"); } }
impl RefM for A { fn refm(&self) { println!("A::refm()"); } }
impl RefM for &&&A { fn refm(&self) { println!("&&&A::refm()"); } }
fn main() {
// I'll use @ to denote left side of the dot operator
(*X{val:42}).m(); // i32::m() , Self == @
X{val:42}.m(); // X::m() , Self == @
(&X{val:42}).m(); // &X::m() , Self == @
(&&X{val:42}).m(); // &&X::m() , Self == @
(&&&X{val:42}).m(); // &&&X:m() , Self == @
(&&&&X{val:42}).m(); // &&&X::m() , Self == *@
(&&&&&X{val:42}).m(); // &&&X::m() , Self == **@
println!("-------------------------");
(*X{val:42}).refm(); // i32::refm() , Self == @
X{val:42}.refm(); // X::refm() , Self == @
(&X{val:42}).refm(); // X::refm() , Self == *@
(&&X{val:42}).refm(); // &X::refm() , Self == *@
(&&&X{val:42}).refm(); // &&X::refm() , Self == *@
(&&&&X{val:42}).refm(); // &&&X::refm(), Self == *@
(&&&&&X{val:42}).refm(); // &&&X::refm(), Self == **@
println!("-------------------------");
Y{val:42}.refm(); // i32::refm() , Self == *@
Z{val:Y{val:42}}.refm(); // i32::refm() , Self == **@
println!("-------------------------");
A.m(); // A::m() , Self == @
// without the Copy trait, (&A).m() would be a compilation error:
// cannot move out of borrowed content
(&A).m(); // A::m() , Self == *@
(&&A).m(); // &&&A::m() , Self == &@
(&&&A).m(); // &&&A::m() , Self == @
A.refm(); // A::refm() , Self == @
(&A).refm(); // A::refm() , Self == *@
(&&A).refm(); // A::refm() , Self == **@
(&&&A).refm(); // &&&A::refm(), Self == @
}
(Playground)
所以,看起来,或多或少:
- 编译器将根据调用方法的需要插入尽可能多的解引用运算符。
- 编译器在解析使用
&self
(按引用调用)声明的方法时: - 首先尝试调用
self
的单个取消引用 - 然后尝试调用
self
的确切类型 - 然后,尝试插入匹配所需数量的取消引用运算符
- 对于
T
类型,使用self
(按值调用)声明的方法的行为就好像它们是使用&T
类型的&self
(按引用调用)声明的,并且在点运算符左侧的任何内容的引用上调用。 - 首先使用raw内置解引用来尝试上述规则,如果不匹配,则使用带有
Deref
trait的重载。
确切的自动解引用规则是什么?有人能给出这样一个设计决策的正式理由吗?
4条答案
按热度按时间jxct1oxe1#
你的伪代码非常正确,对于这个例子,假设我们有一个方法调用
foo.bar()
,其中foo: T
。(FQS)来明确调用方法的类型,例如A::bar(foo)
或A::bar(&***foo)
。我将编写一堆随机的大写字母,每一个都是一些任意的类型/特性,除了T
始终是调用该方法的原始变量foo
的类型。算法的核心是:
1.对于每个"解引用步骤"
U
(即设置U = T
,然后设置U = *T
,...)1.如果有一个方法
bar
,其中接收器类型(方法中self
的类型)与U
完全匹配,则使用它("按值方法")1.否则,添加一个auto-ref(取接收方的
&
或&mut
),如果某个方法的接收方与&U
匹配,则使用它(一个"autorefd方法")值得注意的是,每件事都考虑方法的"接收者类型",* 而不是 * trait的
Self
类型,即impl ... for Foo { fn method(&self) {} }
在匹配方法时考虑&Foo
,而fn method2(&mut self)
在匹配时考虑&mut Foo
。如果内部步骤中有多个trait方法有效(也就是说,1.或2.中的每一个只能有零个或一个trait方法有效,但每一个都可以有一个trait方法有效),这是一个错误:1中的一个将首先被采用),并且固有方法优先于trait方法。如果我们到达循环的末尾而没有找到任何匹配,这也是一个错误。递归
Deref
实现也是一个错误,这使得循环无限(它们将达到"递归极限")。这些规则似乎在大多数情况下都是按我的意思来做的,尽管能够编写明确的FQS形式在某些极端情况下非常有用,并且对于宏生成代码的敏感错误消息也非常有用。
仅添加一个自动引用,因为
&foo
保持到foo
的强连接(它是foo
自身的地址),但是取更多的引用开始丢失它:&&foo
是堆栈上存储&foo
的某个临时变量的地址。示例
假设我们有一个调用
foo.refm()
,如果foo
的类型为:X
,则我们从U = X
开始,refm
的接收器类型为&...
,因此步骤1不匹配,执行自动引用得到&X
,这确实匹配(与Self = X
),因此调用为RefM::refm(&foo)
&X
,以U = &X
开始,它与第一步中的&self
匹配(与Self = X
匹配),因此调用为RefM::refm(foo)
&&&&&X
,这与任何一步都不匹配(没有为&&&&X
或&&&&&X
实现trait),因此我们解引用一次以获得U = &&&&X
,它匹配1(与Self = &&&X
匹配),调用为RefM::refm(*foo)
Z
,不匹配任何一个步骤,所以它被解引用一次,以获得Y
,它也不匹配,所以它被再次解引用,以获得X
,它不匹配1,但在自动引用后匹配,所以调用是RefM::refm(&**foo)
。&&A
,1.不匹配,2.也不匹配,因为没有为&A
(对于1)或&&A
(对于2)实现trait,所以它被解引用到&A
,它将1.与Self = A
匹配假设我们有
foo.m()
,并且A
不是Copy
,如果foo
的类型为:A
,则U = A
与self
直接匹配,因此调用是M::m(foo)
与Self = A
&A
,那么1.不匹配,2.也不匹配(&A
和&&A
都没有实现trait),所以它被解引用到A
,A
匹配,但是M::m(*foo)
需要通过值获取A
,因此移出foo
,因此产生错误。&&A
,1.不匹配,但是自动引用给出了&&&A
,它确实匹配,所以调用是M::m(&foo)
与Self = &&&A
。(This答案是基于the code的,并且相当接近(稍微过时的)自述文件。编译器/语言这一部分的主要作者Niko Matsakis也浏览了这个答案。)
o2g1uqev2#
Rust的引用有a chapter about the method call expression。我复制了下面最重要的部分。提醒:我们讨论表达式
recv.m()
,其中recv
在下面被称为"接收器表达式"。第一步是构建候选接收方类型列表。通过重复解引用接收方表达式的类型,将遇到的每个类型添加到列表中,然后在末尾尝试大小不定的强制,如果成功,则添加结果类型,来获得这些类型。然后,对于每个候选
T
,将&T
和&mut T
添加到列表中,紧跟在T
之后。例如,如果接收者具有类型
Box<[i32;2]>
,则候选类型将是Box<[i32;2]>
、&Box<[i32;2]>
、&mut Box<[i32;2]>
、[i32; 2]
(通过解引用)、&[i32; 2]
、&mut [i32; 2]
、[i32]
(通过无大小强制)、&[i32]
,并且最后是&mut [i32]
。然后,对于每个候选类型
T
,在以下位置搜索具有该类型接收器的可见方法:T
的固有方法(直接在T
[¹]上实现的方法)。1.由
T
实现的可见特征提供的任何方法。[...](* 关于[¹]* 的注解:实际上,我认为这个措辞是错误的。I've opened an issue。让我们忽略括号中的句子。)
让我们从你的代码中详细地看几个例子!对于你的例子,我们可以忽略关于"无大小强制"和"固有方法"的部分。
i32
。我们执行以下步骤:i32
无法取消引用,因此我们已经完成了步骤1。* * 一米二三米一x**&i32
和&mut i32
。列表:* * 一米二十六分一x**i32
的<i32 as M>::m
,所以我们已经完成了。到目前为止都很简单。现在让我们选择一个更难的例子:* *
(&&A).m()
**。接收方表达式的类型为&&A
。我们将执行以下步骤:&&A
可以被解引用为&A
,因此我们将其添加到列表中。&A
可以再次被解引用,因此我们也将A
添加到列表中。A
不能被解引用,因此我们停止。* * 一米三十六分一x**T
,我们在T
之后立即添加&T
和&mut T
。* * 一米四一米一**&&A
的方法,所以我们转到列表中的下一个类型。<&&&A as M>::m
,它确实具有接收器类型&&&A
。下面是所有示例的候选接收器列表。
⟪x⟫
中包含的类型是"won"的类型,即可以找到拟合方法的第一个类型。还要记住,列表中的第一个类型总是接收器表达式的类型。最后,我将列表格式化为三行,但这只是格式化:该列表是平面列表。jgwigjjp3#
这个问题困扰了我很长时间,尤其是这一部分:
直到我找到了一种方法来记住这些奇怪的规则。我不确定这是否正确,但大多数时候这种方法是有效的。
关键是,在寻找使用哪个函数时,不要不要使用调用"点运算符"的类型来确定使用哪个"impl",而是根据 * 函数签名 * 找到函数,然后用 * 函数签名 * 确定"self"的类型。
我将函数定义代码转换如下:
因此,在编写代码时:
第一个月
该函数
fn refm(&X ) { println!("X::refm()");
将调用,因为参数类型为
&X
。如果没有找到匹配的函数签名,则执行auto-ref或某些auto-deref。
j13ufse24#
对于类型T使用self(按值调用)声明的方法,其行为就好像它们是使用类型&T的&self(按引用调用)声明的,并且在点操作符左侧的任何引用上调用。
它们的行为并不完全相同。当使用self时,会发生移动(除非结构体是Copy)