我正在学习/体验 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 == @
}
因此,似乎或多或少:
编译器将根据需要插入尽可能多的取消引用运算符来调用方法。
编译器在解析使用 &self (call-by-reference) 声明的方法时: 首先尝试调用 self 的单个取消引用 然后尝试调用 self 的确切类型 然后尝试插入匹配所需的尽可能多的取消引用运算符
首先尝试调用 self 的单个取消引用
然后尝试调用 self 的确切类型
然后,尝试为匹配插入尽可能多的取消引用运算符
对 T 类型使用 self(按值调用)声明的方法的行为就像它们对类型 &T 使用 &self(按引用调用)声明并调用对点运算符左侧的任何内容的引用。
上述规则首先尝试使用原始内置解引用,如果没有匹配,则使用具有 Deref 特征的重载。
确切的自动取消引用规则是什么?任何人都可以为这样的设计决定提供任何正式的理由吗?
您的伪代码非常正确。对于此示例,假设我们有一个方法调用 foo.bar()
,其中 foo: T
。我将使用 fully qualified syntax (FQS) 来明确调用方法的类型,例如 A::bar(foo)
或 A::bar(&***foo)
。我只是要写一堆随机的大写字母,每个都只是一些任意类型/特征,除了 T
始终是调用该方法的原始变量 foo
的类型。
该算法的核心是:
对于每个“取消引用步骤”U(即设置 U = T 然后设置 U = *T,...),如果有一个方法栏,其中接收器类型(方法中的 self 类型)与 U 完全匹配,请使用它(“按值方法”)否则,添加一个自动引用(接收器的 & 或 &mut),并且,如果某个方法的接收器与 &U 匹配,则使用它(“自动引用方法”)
如果有一个方法栏,其中接收者类型(方法中的 self 类型)与 U 完全匹配,请使用它(“按值方法”)
否则,添加一个自动引用(接收器的 & 或 &mut),如果某个方法的接收器与 &U 匹配,则使用它(“自动引用方法”)
值得注意的是,一切都考虑方法的“接收者类型”,不是特征的 Self
类型,即 impl ... for Foo { fn method(&self) {} }
在匹配方法时会考虑 &Foo
,而 fn method2(&mut self)
会认为匹配时大约 &mut Foo
。
如果在内部步骤中有多个有效的 trait 方法是错误的(也就是说,在 1. 或 2. 中的每一个中只能有零个或一个有效的 trait 方法,但每个都可以有一个有效:一个from 1 将首先被采用),并且固有方法优先于特征方法。如果我们在循环结束时没有找到任何匹配的东西,这也是一个错误。具有递归 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 实现),所以我们取消引用一次以获得 U = &&&&X,它匹配 1(与 Self = &&&X)并且调用是 RefM::refm( *富)
Z, 不匹配任何一个步骤,所以它被取消引用一次,得到 Y,它也不匹配,所以它再次被取消引用,得到 X,它不匹配 1,但在 autorefing 之后匹配,所以调用是 RefM::refm(&**foo)。
&&A,1. 不匹配,2. 也不匹配,因为没有为 &A(对于 1)或 &&A(对于 2)实现特征,所以它被取消引用到与 1. 匹配的 &A,Self = A
假设我们有 foo.m()
,并且 A
不是 Copy
,如果 foo
具有类型:
A,然后 U = A 直接匹配 self 所以调用是 M::m(foo) 与 Self = A
&A,然后 1. 不匹配,2. 也不匹配(&A 和 &&A 都没有实现特征),所以它被取消引用到匹配的 A,但是 M::m(*foo) 需要按值获取 A因此移出 foo,因此出现错误。
&&A, 1. 不匹配,但 autorefing 给出 &&&A,它确实匹配,所以调用是 M::m(&foo) 与 Self = &&&A。
(此答案基于 the code 和 is reasonably close to the (slightly outdated) README。这部分编译器/语言的主要作者 Niko Matsakis 也浏览了此答案。)
Rust 引用有 a chapter about the method call expression。我复制了下面最重要的部分。提醒:我们讨论的是表达式recv.m()
,其中recv
在下面被称为“接收者表达式”。
第一步是建立一个候选接收器类型列表。通过反复取消引用接收器表达式的类型,将遇到的每种类型添加到列表中,最后尝试在末尾进行未调整大小的强制,如果成功则添加结果类型来获得这些。然后,对于每个候选 T,在 T 之后立即将 &T 和 &mut 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 [¹] 上实现的方法)。由 T 实现的可见特征提供的任何方法。 [...]
(关于 [¹] 的注意事项:我实际上认为这个措辞是错误的。I've opened an issue。让我们忽略括号中的那句话。)
让我们详细浏览您的代码中的几个示例!对于您的示例,我们可以忽略有关“无大小强制”和“固有方法”的部分。
(*X{val:42}).m()
:接收者表达式的类型是 i32
。我们执行以下步骤:
创建候选接收器类型列表:i32 不能被取消引用,所以我们已经完成了第 1 步。列表:[i32] 接下来,我们添加 &i32 和 &mut i32。列表:[i32, &i32, &mut i32]
i32 不能被取消引用,所以我们已经完成了第 1 步。列表:[i32]
接下来,我们添加 &i32 和 &mut i32。列表:[i32, &i32, &mut i32]
搜索每个候选接收器类型的方法:我们找到具有接收器类型 i32 的
我们发现
到目前为止很容易。现在让我们选择一个更难的例子:(&&A).m()
。接收者表达式的类型是 &&A
。我们执行以下步骤:
创建候选接收器类型列表:&&A 可以取消引用到 &A,因此我们将其添加到列表中。 &A 可以再次取消引用,因此我们还将 A 添加到列表中。 A 不能被取消引用,所以我们停止。 List: [&&A, &A, A] 接下来,对于列表中的每个类型 T,我们在 T 之后立即添加 &T 和 &mut T。 List: [&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
&&A 可以取消对 &A 的引用,因此我们将其添加到列表中。 &A 可以再次取消引用,因此我们还将 A 添加到列表中。 A 不能被取消引用,所以我们停止。列表:[&&A,&A,A]
接下来,对于列表中的每个类型 T,我们在 T 之后立即添加 &T 和 &mut T。列表:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
为每个候选接收器类型搜索方法:没有接收器类型为 &&A 的方法,因此我们转到列表中的下一个类型。我们发现方法 <&&&A as M>::m 确实具有接收器类型 &&&A。所以我们完成了。
没有接收器类型 &&A 的方法,所以我们转到列表中的下一个类型。
我们发现方法 <&&&A as M>::m 确实具有接收器类型 &&&A。所以我们完成了。
以下是您所有示例的候选接收者列表。 ⟪x⟫
中包含的类型是“获胜”的类型,即第一个可以找到拟合方法的类型。还要记住,列表中的第一个类型始终是接收者表达式的类型。最后,我将列表格式化为三行,但这只是格式化:这个列表是一个平面列表。
(*X{val:42}).m() →
X{val:42}.m() →
(&X{val:42}).m() → <&X as M>::m [⟪&X⟫, &&X, &mut &X, X, &X, &mut X, i32, &i32, &mut i32]
(&&X{val:42}).m() → <&&X as M>::m [⟪&&X⟫, &&&X, &mut &&X, &X, &&X, &mut &X, X, &X, &mut X, i32, &i32, &mut i32]
(&&&X{val:42}).m() → <&&&X as M>::m [⟪&&&X⟫, &&&&X, &mut &&&X, &&X, &&&X, &mut &&X, &X, &&X, &mut &X, X, &X, &mut X, i32, &i32, &mut i32]
(&&&&X{val:42}).m() → <&&&X as M>::m [&&&&X, &&&&&X, &mut &&&&X, ⟪&&&X⟫, &&&&X, &mut &&&X, &&X, &&&X, &mut &&X, &X, &&X, &mut &X, X, &X, &mut X, i32, &i32, &mut i32]
(&&&&&X{val:42}).m() → <&&&X as M>::m [&&&&&X, &&&&&&X, &mut &&&&&X, &&&&X, &&&&&X, &mut &&&&X, ⟪&&&X⟫, &&&&X, &mut &&&X, &&X, &&&X, &mut &&X, &X, &&X, &mut &X, X, &X, &mut X, i32, &i32, &mut i32]
(*X{val:42}).refm() →
X{val:42}.refm() →
(&X{val:42}).refm() →
(&&X{val:42}).refm() → <&X as RefM>::refm [⟪&&X⟫, &&&X, &mut &&X, &X, &&X, &mut &X, X, &X, &mut X, i32, &i32, &mut i32]
(&&&X{val:42}).refm() → <&&X as RefM>::refm [⟪&&&X⟫, &&&&X, &mut &&&X, &&X, &&&X, &mut &&X, &X, &&X, &mut &X, X, &X, &mut X, i32, &i32, &mut i32]
(&&&&X{val:42}).refm() → <&&&X as RefM>::refm [⟪&&&&X⟫, &&&&&X, &mut &&&&X, &&&X, &&&&X, &mut &&&X, &&X, &&&X, &mut &&X, &X, &&X, &mut &X, X, &X, &mut X, i32, &i32, &mut i32]
(&&&&&X{val:42}).refm() → <&&&X as RefM>::refm [&&&&&X, &&&&&&X, &mut &&&&&X, ⟪&&&&X⟫, &&&&&X, &mut &&&&X, &&&X, &&&&X, &mut &&&X, &&X, &&&X, &mut &&X, &X, &&X, &mut &X, X, &X, &mut X, i32, &i32, &mut i32]
Y{val:42}.refm() →
Z{val:Y{val:42}}.refm() →
Am() → ::m [⟪A⟫, &A, &mut A]
(&A).m() → ::m [&A, &&A, &mut &A, ⟪A⟫, &A, &mut A]
(&&A).m() → <&&&A as M>::m [&&A, ⟪&&&A⟫, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
(&&&A).m() → <&&&A as M>::m [⟪&&&A⟫, &&&&A, &mut &&&A, &&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
A.refm() → ::refm [A, ⟪&A⟫, &mut A]
(&A).refm() → ::refm [⟪&A⟫, &&A, &mut &A, A, &A, &mut A]
(&&A).refm() → ::refm [&&A, &&&A, &mut &&A, ⟪&A⟫, &&A, &mut &A, A, &A, &mut A]
(&&&A).refm() → <&&&A as RefM>::refm [&&&A, ⟪&&&&A⟫, &mut &&&A, &&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
这个问题困扰了我很久,尤其是这部分:
(*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 == **@
直到我找到一种方法来记住这些奇怪的规则。我不确定这是否正确,但大多数时候这种方法是有效的。
关键是,在查找使用哪个函数时,不要使用调用“点运算符”的类型来确定使用哪个“impl”,而是根据函数签名找到函数,然后确定“ self" 带有函数签名。
我将函数定义代码转换如下:
trait RefM { fn refm(&self); }
impl RefM for i32 { fn refm(&self) { println!("i32::refm()"); } }
// converted to: fn refm(&i32 ) { println!("i32::refm()"); }
// => type of 'self' : i32
// => type of parameter: &i32
impl RefM for X { fn refm(&self) { println!("X::refm()"); } }
// converted to: fn refm(&X ) { println!("X::refm()"); }
// => type of 'self' : X
// => type of parameter: &X
impl RefM for &X { fn refm(&self) { println!("&X::refm()"); } }
// converted to: fn refm(&&X ) { println!("&X::refm()"); }
// => type of 'self' : &X
// => type of parameter: &&X
impl RefM for &&X { fn refm(&self) { println!("&&X::refm()"); } }
// converted to: fn refm(&&&X ) { println!("&&X::refm()"); }
// => type of 'self' : &&X
// => type of parameter: &&&X
impl RefM for &&&X { fn refm(&self) { println!("&&&X::refm()"); } }
// converted to: fn refm(&&&&X) { println!("&&&X::refm()"); }
// => type of 'self' : &&&X
// => type of parameter: &&&&X
因此,当您编写代码时:
(&X{val:42}).refm();
功能
fn refm(&X ) { println!("X::refm()");
将被调用,因为参数类型是 &X
。
如果没有找到匹配的函数签名,则执行自动引用或一些自动取消引用。
&&String
->&String
->String
->str
),然后最多引用一次 (str
->&str
)”。