什么是Rust的确切的自动引用规则?

我正在学习/试验Rust,在我用这种语言发现的所有优雅中,有一个让我感到困惑的特性,似乎完全不合适。

在进行方法调用时,Rust会自动解引用指针。 我做了一些testing,以确定确切的行为:

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<'a> M for &'a X { fn m(self) { println!("&X::m()"); } } impl<'a, 'b> M for &'a &'b X { fn m(self) { println!("&&X::m()"); } } impl<'a, 'b, 'c> M for &'a &'b &'c 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<'a> RefM for &'a X { fn refm(&self) { println!("&X::refm()"); } } impl<'a, 'b> RefM for &'a &'b X { fn refm(&self) { println!("&&X::refm()"); } } impl<'a, 'b, 'c> RefM for &'a &'b &'c 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 } } struct A; impl std::marker::Copy for A {} impl M for A { fn m(self) { println!("A::m()"); } } impl<'a, 'b, 'c> M for &'a &'b &'c A { fn m(self) { println!("&&&A::m()"); } } impl RefM for A { fn refm(&self) { println!("A::refm()"); } } impl<'a, 'b, 'c> RefM for &'a &'b &'c 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::refm() , 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 == **@ (*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 == **@ Y{val:42}.refm(); // i32::refm() , self == *@ Z{val:Y{val:42}}.refm(); // i32::refm() , self == **@ Am(); // 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 == @ } 

所以,似乎或多或less:

  • 编译器将根据需要插入尽可能多的解引用运算符来调用方法。
  • 编译器在parsing使用&self (call-by-reference)声明的方法时:
    • 首先尝试呼吁对self进行一次解除
    • 然后尝试调用self的确切types
    • 然后,尝试插入尽可能多的解除引用运算符来进行匹配
  • 使用typesT self (call-by-value)声明的方法就像使用&self (call-by-reference)声明types为&T并且调用点运算符左侧的引用。
  • 上述规则首先使用原始的内置解引用来尝试,如果不匹配,则使用Deref特性的重载。

什么是确切的自动解除引用规则? 任何人都可以为这样的devise决定提供任何正式的理由吗?

你的伪代码是非常正确的。 对于这个例子,假设我们有一个方法调用foo.bar() ,其中foo: T 。 我将使用完全限定的语法 (FQS)来清楚地说明方法被调用的types,例如A::bar(foo)A::bar(&***foo) 。 我只是要写一堆随机的大写字母,每个都只是一些任意的types/特性,除了T总是这个方法被调用的原始variablesfoo的types。

algorithm的核心是:

  • 对于每个“取消引用步骤” U (即设置U = T ,然后U = *T ,…)
    1. 如果有一个方法bar ,接收方types(方法中的selftypes)与U完全匹配,则使用它( “按值方法” )
    2. 否则,添加一个auto-ref(接收器的&&mut ),如果某个方法的接收器匹配&U ,则使用它( 一个“autorefd方法” )

值得注意的是,一切都认为方法的“接收器types”, 而不是该特性的Selftypes,即impl ... for Foo { fn method(&self) {} }在匹配方法时考虑&Foo ,而fn method2(&mut self)在匹配时会考虑&mut Foo

如果在内部步骤中有多个有效的trait方法是错误的(也就是说,在1或2中只能有零个或一个有效的特征方法,但是每个方法都可以有一个有效:从1开始),固有方法优先于特征方法。 如果我们到达循环的结尾而没有find任何匹配的东西,那也是一个错误。 recursion的Deref实现也是一个错误,这使得循环无限(它们会达到“recursion限制”)。

这些规则似乎在大多数情况下都是这样做的,虽然有能力编写明确的FQS表单在一些边缘情况下非常有用,对于macros生成的代码也是非常有用的错误消息。

只有一个自动引用被添加,因为

  • 如果没有绑定,事情变得很糟糕/缓慢,因为每个types可以有任意数量的引用
  • 以一个引用&foo保留与foo (它是foo本身的地址)的强有力的联系,但是更多的开始丢失它: &&foo是存储&foo的栈上的一些临时variables的地址。

例子

假设我们有一个调用foo.refm() ,如果footypes为:

  • X ,那么我们从U = X开始, refm有接收器types&... ,所以步骤1不匹配,以auto-ref给我们&X ,这匹配(与Self = X ),所以调用是RefM::refm(&foo)
  • &X ,从U = &X ,它与第一步( Self = X )中的&self匹配,所以调用是RefM::refm(foo)
  • &&&&&X ,这两个步骤都不匹配(该特征不适用于&&&&X&&&&&X ),因此我们取消引用一次以获得U = &&&&X ,匹配1(与Self = &&&X ),调用RefM::refm(*foo)
  • Z ,不匹配任何一步,所以它被解除引用一次,得到Y ,这也不匹配,所以它被再次解除,得到X ,这不匹配1,但在自动修复后匹配,所以调用是RefM::refm(&**foo)
  • &&A ,1.不匹配,因为这个特征没有针对&A (针对1)或&&A (针对2)实施,所以它被解除引用到&A相匹配的&A ,其中Self = A

假设我们有foo.m() ,并且A不是Copy ,如果footypes为:

  • 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

(这个答案是以代码为基础的 ,并且相当接近(稍微过时的)自述文件),这部分编译器/语言的主要作者Niko Matsakis也浏览了这个答案。)