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


180

我正在使用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
    • 然后,尝试插入匹配所需的尽可能多的取消引用运算符
  • self对于类型使用(value-by-value)声明的方法,其T行为就像是对类型使用&self(call-by-reference)声明的方法一样,&T并在对点运算符左侧任何内容的引用上进行调用。
  • 上面的规则首先使用原始的内置解引用进行尝试,如果没有匹配项,Deref则使用带有trait 的重载。

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


我将其交叉发布到Rust subreddit,希望得到一些好的答案!
Shepmaster

为了获得更多乐趣,请尝试使用泛型重复实验并比较结果。
user2665887 2015年

Answers:


137

您的伪代码几乎是正确的。对于此示例,假设我们有一个方法调用foo.bar()where foo: T。我将使用完全限定的语法(FQS)来明确指出使用哪种类型的方法,例如A::bar(foo)or A::bar(&***foo)。我只是要写一堆随机的大写字母,每个字母都是任意的类型/特征,除了T总是foo调用该方法的原始变量的类型。

该算法的核心是:

  • 对于每个“取消引用步骤” U(即先设置U = T,然后再U = *T...)
    1. 如果存在一个bar接收方类型(方法中的类型selfU完全匹配的方法,请使用它(“按值方法”
    2. 否则,添加一个自动引用(接收者的&&mut),如果某种方法的接收者匹配&U,则使用它(“自动引用方法”

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

如果内部步骤中有多个有效的特征方法(即,在1.或2中的每一个中只能有零个或一个有效的特征方法,但是对于每个中有一个有效的特征方法,这是一个错误)。从1开始),并且固有方法优先于特征方法。如果我们没有找到任何匹配的内容而进入循环的结尾,这也是一个错误。拥有递归Deref实现也是一个错误,这会使循环无限(它们将达到“递归限制”)。

这些规则在大多数情况下似乎是“按我所想”的方式,尽管具有写明确的FQS格式的能力在某些极端情况下以及对于宏生成代码的明智错误消息非常有用。

仅添加一个自动引用,因为

  • 如果没有限制,事情就会变糟/变慢,因为每种类型都可以有任意数量的引用
  • 引用一个&foo保留了foo与之的牢固联系(它是其foo自身的地址),但引用更多则开始失去它:&&foo是存储在的堆栈上某个临时变量的地址&foo

例子

假设我们有一个call foo.refm(),如果foo有类型:

  • X,那么我们从开始U = Xrefm具有接收器类型&...,因此第1步不匹配,采用auto-ref给我们&X,并且与(匹配Self = X)匹配,因此调用是RefM::refm(&foo)
  • &X,以开头U = &X,与&self第一步匹配 (为Self = X),因此调用为RefM::refm(foo)
  • &&&&&X,此步骤与任一步骤都不匹配(特征未针对&&&&X或实现&&&&&X),因此我们取消对get的引用一次U = &&&&X,该方法与1(带有Self = &&&X)匹配,并且调用为RefM::refm(*foo)
  • Z,都不匹配任一步骤,因此对它进行一次解引用,即get Y,它也不匹配,因此再次对其进行解引用,以获取get X,其不匹配1,但在自动引用后匹配,因此调用为RefM::refm(&**foo)
  • &&A,则1.不匹配,也不匹配2.,因为trait未针对&A(for 1)或&&A(for 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 Gives &&&A确实匹配,因此调用M::m(&foo)Self = &&&A

(此答案是基于代码的,并且与(略有过时的)自述文件相当接近。编译器/语言这一部分的主要作者Niko Matsakis也浏览了该答案。)


15
这个答案似乎详尽而详尽,但是我认为它缺乏规则的简短而易懂的总结。Shepmaster在此评论中给出了这样一个总结:“ [deref算法]将尽可能多地进行deref(&&String-> &String-> String-> str),然后最大引用一次(str-> &str)”。
Lii

(我不知道自己的解释是多么准确和完整。)
Lii

1
在什么情况下会发生自动取消引用?它仅用于方法调用的接收方表达式吗?还可以进行现场访问吗?作业右侧?左手边?功能参数?返回值表达式?
Lii

1
注意:目前,nomicon有一个TODO注释,可以从此答案中窃取信息并将其写在static.rust-lang.org/doc/master/nomicon/dot-operator.html
SamB,

1
在此算法的每个步骤中,强制(A)在此之前尝试过,还是(B)在此之后尝试过,还是(C)在此算法的每个步骤中尝试过,还是(D)其他?
haslersn

8

Rust参考书中有一章是关于方法调用表达式的。我在下面复制了最重要的部分。提醒:我们正在谈论一个表达式recv.m()recv以下称为“接收者表达式”。

第一步是建立候选接收器类型的列表。通过重复引用接收方表达式的类型,将遇到的每种类型添加到列表中,然后最后尝试在结尾处尝试不定大小的强制,然后添加结果类型(如果成功的话)来获取这些内容。然后,对于每个候选人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]通过未施胶胁迫),&[i32]和最后&mut [i32]

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

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

有关[¹]的注意事项:我实际上认为此措词是错误的。我已经提出了一个问题。让我们忽略括号中的那句话。)


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

(*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]
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.