rust的自动解引用规则是什么?

我正在学习/试验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(引用调用)声明的方法时:
    • 首先尝试调用self的单个解引用
    • 然后尝试调用self的确切类型
    • 然后,尝试为匹配插入尽可能多的解引用操作符
    • 李< / ul > < / >
    • 使用self(按值调用)为T类型声明的方法的行为就像使用&self(按引用调用)为&T类型声明的方法一样,并在点操作符左侧的引用上调用。
    • 上面的规则首先使用原始的内置解引用进行尝试,如果没有匹配,则使用带有Deref特征的重载。

    确切的自动解引用规则是什么?有人能给出这样一个设计决策的正式理由吗?

46290 次浏览

您的伪代码非常正确。在这个例子中,假设我们有一个方法调用foo.bar(),其中foo: T. c。我将使用完全限定语法 (FQS)来明确调用方法的类型,例如A::bar(foo)A::bar(&***foo)。我只是要写一堆随机的大写字母,每个字母都是一些任意的类型/trait,除了T始终是调用该方法的原始变量foo的类型。

算法的核心是:

  • 对于每个“废弃step" U(即设置U = T,然后设置U = *T,…)
    1. 如果有一个方法bar,其中接收者类型(方法中self的类型)完全匹配U,则使用它(“按值方法”;)
    2. 否则,添加一个auto-ref(获取接收器的&&mut),并且,如果某个方法的接收器匹配&U,则使用它(“自动刷新方法”;)

值得注意的是,所有内容都考虑“接收者类型”;是该方法的Self类型,即impl ... for Foo { fn method(&self) {} }在匹配方法时考虑&Foo,而fn method2(&mut self)在匹配时考虑&mut Foo

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

在大多数情况下,这些规则似乎都是按照我的意思来做的,尽管能够编写明确的FQS表单在某些边缘情况下非常有用,对于宏生成代码的合理错误消息也非常有用。

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

  • 如果没有约束,事情就会变得糟糕/缓慢,因为每种类型都可以有任意数量的引用
  • 取一个引用&foo会保留与foo的强连接(它是foo本身的地址),但取更多引用会开始失去它:&&foo是堆栈上存储&foo的某个临时变量的地址。

例子

假设我们有一个调用foo.refm(),如果foo的类型是:

  • X,然后我们从U = X开始,refm有接收器类型&...,所以第一步不匹配,采用自动引用给我们&X,这确实匹配(与Self = X),所以调用RefM::refm(&foo)
  • &XU = &X开始,它在第一步中匹配&self(使用Self = X),因此调用为RefM::refm(foo)
  • &&&&&X,这与任何一个步骤都不匹配(特征没有为&&&&X&&&&&X实现),所以我们解引用一次以获得U = &&&&X,它与1匹配(与Self = &&&X匹配),并且调用为RefM::refm(*foo)
  • Z,这两步都不匹配,所以它被解引用一次,得到Y,它也不匹配,所以它被再次解引用,得到X,它不匹配1,但在自动引用后匹配,所以调用是RefM::refm(&**foo)
  • &&A, 1。不匹配2也不匹配。因为该trait没有为&A (for 1)或&&A (for 2)实现,所以它被解引用到匹配1的&A。,用Self = A

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

  • A,然后U = A直接匹配self,因此调用是M::m(foo)Self = A
  • &A, then 1。不匹配,2也不匹配。(&A&&A都没有实现该trait),因此它被解引用到A,后者确实匹配,但M::m(*foo)需要按值取A,因此移出foo,因此出现错误。
  • &&A 1。不匹配,但自动引用给出了匹配的&&&A,因此调用是M::m(&foo)Self = &&&A

(这个答案是基于的代码相当接近(略过时)README。这部分编译器/语言的主要作者Niko Matsakis也浏览了一下这个答案。)

Rust引用有关于方法调用表达式的一章。我复制了下面最重要的部分。提醒:我们讨论的是表达式recv.m(),其中recv在下面被称为“接收者表达式”。

第一步是构建候选接收器类型列表。通过重复对接收方表达式的类型进行解引用,将遇到的每个类型添加到列表中,然后最后尝试unsize强制,如果成功则添加结果类型。然后,对于每个候选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](通过unsized强制),&[i32],最后是&mut [i32]

然后,对于每个候选类型T,在以下位置搜索具有该类型接收器的可见方法:

  1. T的固有方法(直接在T[¹]上实现的方法)。
  2. T实现的可见trait所提供的任何方法。[…]

关于[¹]的说明:实际上我认为这种措辞是错误的。我开了一期。让我们忽略括号里的那句话。)


让我们详细地看一下代码中的几个示例!对于您的示例,我们可以忽略关于“unsized强制”和“固有方法”的部分。

(*X{val:42}).m():接收方表达式的类型是i32。我们执行以下步骤:

  • 创建候选接收类型列表:
    • i32不能被解引用,所以我们已经完成了第一步。列表:[i32]
    • 接下来,我们添加&i32&mut i32。列表:[i32, &i32, &mut i32]
    • 李< / ul > < / >
    • 搜索每个候选接收器类型的方法:
      • 我们找到接收器类型为i32<i32 as M>::m。我们已经做完了。
      • 李< / ul > < / >

      < br >

      到目前为止还很简单。现在让我们选择一个更难的例子:(&&A).m()。接收方表达式的类型是&&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]
        • 李< / ul > < / >
        • 搜索每个候选接收器类型的方法:
          • 没有接收器类型为&&A的方法,因此我们转到列表中的下一个类型。
          • 我们找到方法<&&&A as M>::m,它确实具有接收器类型&&&A。我们做完了。
          • 李< / ul > < / >

          这是您所有示例的候选接收者列表。⟪x⟫中包含的类型是“won”的类型,即可以找到拟合方法的第一个类型。还要记住,列表中的第一个类型始终是接收方表达式的类型。最后,我将列表格式化为三行,但这只是格式化:这个列表是一个平面列表。

          • (*X{val:42}).m()<i32 as M>::m
            [⟪i32⟫, &i32, &mut i32]
            
          • X{val:42}.m()<X as M>::m
            [⟪X⟫, &X, &mut X,
            i32, &i32, &mut i32]
            
          • (&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()<i32 as RefM>::refm
            [i32, ⟪&i32⟫, &mut i32]
            
          • X{val:42}.refm()<X as RefM>::refm
            [X, ⟪&X⟫, &mut X,
            i32, &i32, &mut i32]
            
          • (&X{val:42}).refm()<X as RefM>::refm
            [⟪&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,
            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()<i32 as RefM>::refm
            [Y, &Y, &mut Y,
            i32, ⟪&i32⟫, &mut i32]
            
          • Z{val:Y{val:42}}.refm()<i32 as RefM>::refm
            [Z, &Z, &mut Z,
            Y, &Y, &mut Y,
            i32, ⟪&i32⟫, &mut i32]
            


          • A.m()<A as M>::m
            [⟪A⟫, &A, &mut A]
            
          • (&A).m()<A as 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()<A as RefM>::refm
            [A, ⟪&A⟫, &mut A]
            
          • (&A).refm()<A as RefM>::refm
            [⟪&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).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 == **@

直到我找到了记住这些奇怪规则的方法。我不确定这是否正确,但大多数时候这种方法是有效的。

关键是,在寻找要使用的函数时,使用调用"点操作符"要确定哪个“;要使用,但根据函数签名找到函数,然后确定"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

如果没有找到匹配的函数签名,则执行自动引用或一些自动deref。