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

时间:2015-02-14 20:31:55

标签: reference dereference formal-semantics 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<'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 == **@

    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(按值调用)为类型T声明的方法就像使用&self(按引用调用)声明类型&T一样调用点运算符左侧的引用。
  • 上述规则首先尝试使用原始内置解除引用,如果没有匹配,则使用带有Deref特征的重载。

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

3 个答案:

答案 0 :(得分:110)

您的伪代码非常正确。对于此示例,假设我们有一个方法调用foo.bar(),其中foo: T。我将使用fully qualified syntax(FQS)明确指出调用方法的类型,例如: A::bar(foo)A::bar(&***foo)。我只是要写一堆随机大写字母,每一个都只是一些任意类型/特征,除了T始终是调用该方法的原始变量foo的类型上。

算法的核心是:

  • 对于每个"dereference step" U(即设置U = T然后设置U = *T,...)
    1. 如果方法bar中接收器类型(方法中self的类型)与U完全匹配,请使用它(a "by value method")< / LI>
    2. 否则,添加一个自动引用(接收&&mut接收器),如果某个方法的接收器与&U匹配,则使用它({{ 3}})

值得注意的是,一切都考虑了&#34;接收器类型&#34;方法,特征的Self类型,即impl ... for Foo { fn method(&self) {} }在匹配方法时考虑&Foo,而fn method2(&mut self)会考虑匹配时&mut Foo

如果在内部步骤中有多个特征方法有效,那么这是一个错误(也就是说,在1.或2.每个中只有零个或一个特征方法有效,但是可以有一个对每个都有效:1中的一个将首先被采用,并且固有方法优先于特征方法。如果我们到达循环的末尾而没有找到匹配的东西,那也是一个错误。具有递归Deref实现也是错误的,这使得循环无限(它们会点击&#34;递归限制&#34;)。

这些规则似乎在大多数情况下都是这样做的,尽管能够编写明确的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未实现特征),因此我们取消引用一次以获得{{1} },与1匹配(U = &&&&X),调用为Self = &&&X
  • RefM::refm(*foo),不会匹配任何一个步骤,因此会被解除引用一次,以获得Z,这也不匹配,因此它再次被取消引用,获取Y,其中不匹配1,但在自动恢复后匹配,因此呼叫为X
  • RefM::refm(&**foo),1。不匹配,2也不匹配,因为&&A(1)或&A(2)没有实现特征,所以它被取消引用&&A,其匹配1.,&A

如果Self = A有类型,则假设我们有foo.m(),而A不是Copy

  • foo,然后A直接与U = A匹配,因此通过self
  • 拨打M::m(foo)
  • Self = A,然后1.不匹配,2也不匹配(&A&A都没有实现特征),所以它被解除引用{{1 }},匹配,但&&A要求按值A移出M::m(*foo),因此错误。
  • A,1。无法匹配,但自动归档会提供匹配的foo,因此呼叫为&&A &&&A

(此答案基于an "autorefd method"the code。编辑/语言的这一部分的主要作者Niko Matsakis也浏览了这个答案。)

答案 1 :(得分:2)

Rust参考具有a chapter about the method call expression。我在下面复制了最重要的部分。提醒:我们正在谈论表达式recv.m(),其中recv在下面称为“接收者表达式”。

  

第一步是建立候选接收者类型的列表。通过重复引用接收方表达式的类型,将遇到的每种类型添加到列表中,然后最后尝试在结尾处尝试不定大小的强制,然后添加结果类型(如果成功)来获取这些内容。然后,对于每个候选人T,在&T之后立即将&mut TT添加到列表中。

     

例如,如果接收者的类型为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,在以下位置使用该类型的接收器搜索可见的方法:

     
      
  1. T的固有方法(直接在T [¹]上实现的方法)。
  2.   
  3. T实现的可见特征提供的任何方法。 [...]
  4.   

关于[¹] 的说明:我实际上认为此措词是错误的。I've opened an issue。我们只需忽略括号中的那句话。)


让我们详细阅读代码中的一些示例!对于您的示例,我们可以忽略有关“不定大小强制”和“固有方法”的部分。

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

  • 创建候选接收者类型的列表:
    • i32无法取消引用,因此我们已经完成了步骤1。列表: [i32]
    • 接下来,我们添加&i32&mut i32。列表: [i32, &i32, &mut i32]
  • 搜索每种候选接收者类型的方法:
    • 我们发现<i32 as M>::m的接收者类型为i32。我们已经完成了。


到目前为止很容易。现在,让我们选择一个更困难的示例: (&&A).m() 。接收方表达式的类型为&&A。我们执行以下步骤:

  • 创建候选接收者类型的列表:
      可以将
    • &&A引用到&A,因此我们将其添加到列表中。 &A可以再次取消引用,因此我们也将A添加到列表中。 A无法取消引用,因此我们停止。列表: [&&A, &A, A]
    • 接下来,对于列表中的每种类型T,我们在&T之后立即添加&mut TT。列表: [&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • 搜索每种候选接收者类型的方法:
    • 没有接收者类型为&&A的方法,因此我们转到列表中的下一个类型。
    • 我们发现方法<&&&A as M>::m确实具有接收者类型&&&A。我们完成了。

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

  • (*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]
    

答案 2 :(得分:0)

我被这个问题困扰了很久,特别是这部分:

    (*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

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