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

What are Rust's exact auto-dereferencing rules?

我 learning/experimenting 喜欢 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)

所以,似乎,或多或少:

确切的自动取消引用规则是什么?任何人都可以为这样的设计决定给出任何正式的理由吗?

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

算法的核心是:

  • 对于每个"dereference step" U(即设置U = T然后U = *T, ...)
    1. 如果有一个方法bar接收者类型(方法中self的类型)完全匹配U,使用它(a "by value method"
    2. 否则,添加一个自动引用(接收者的 &&mut),并且,如果某个方法的接收者匹配 &U,则使用它(an "autorefd method")

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

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

这些规则在大多数情况下似乎都符合我的意思,尽管在某些边缘情况下能够编写明确的 FQS 形式非常有用,并且对于宏生成代码的合理错误消息也是如此。

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

  • 如果没有界限,事情就会变得bad/slow,因为每种类型都可以有任意数量的引用
  • 获取一个引用 &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 (with Self = &&&X) 调用是 RefM::refm(*foo)
  • Z,在任何一步都不匹配,所以它被取消引用一次,得到 Y,它也不匹配,所以它再次被取消引用,得到 X ,它不匹配 1,但在自动引用后匹配,所以调用是 RefM::refm(&**foo).
  • &&A,1. 不匹配,2. 也不匹配,因为 &A(对于 1)或 &&A(对于 2)没有实现特征,所以它被取消引用到 &A,它匹配 1.,与 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, and is reasonably close to the (slightly outdated) README。compiler/language这部分的主要作者Niko Matsakis也浏览了此答案。)

Rust 参考有 a chapter about the method call expression。我复制了下面最重要的部分。提醒:我们说的是表达式recv.m(),其中recv在下面称为"receiver expression"。

The first step is to build a list of candidate receiver types. Obtain these by repeatedly dereferencing the receiver expression's type, adding each type encountered to the list, then finally attempting an unsized coercion at the end, and adding the result type if that is successful. Then, for each candidate T, add &T and &mut T to the list immediately after T.

For instance, if the receiver has type Box<[i32;2]>, then the candidate types will be Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2] (by dereferencing), &[i32; 2], &mut [i32; 2], [i32] (by unsized coercion), &[i32], and finally &mut [i32].

Then, for each candidate type T, search for a visible method with a receiver of that type in the following places:

  1. T's inherent methods (methods implemented directly on T [¹]).
  2. Any of the methods provided by a visible trait implemented by T. [...]

(关于 [¹] 的注意事项:我实际上认为这种措辞是错误的。I've opened an issue。让我们忽略括号中的那句话。)


让我们详细看一下您的代码中的几个示例!对于您的示例,我们可以忽略有关 "unsized coercion" 和 "inherent methods".

的部分

(*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 之后立即添加 &T&mut T。列表:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • 正在为每个候选接收器类型搜索方法:
    • 没有接收器类型为 &&A 的方法,因此我们转到列表中的下一个类型。
    • 我们发现方法<&&&A as M>::m确实有接收者类型&&&A。这样我们就完成了。

以下是所有示例的候选接收者列表。 ⟪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 == **@

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

关键是,在寻找使用哪个函数时,不要使用调用“点运算符”的类型来确定使用哪个“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.

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