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

kFY*_*tek 152 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 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 == @
}
Run Code Online (Sandbox Code Playgroud)

所以,似乎或多或少:

  • 编译器将根据需要插入尽可能多的解引用运算符来调用方法.
  • 编译器在解析使用&self(call-by-reference)声明的方法时:
    • 首先尝试要求单个解除引用 self
    • 然后尝试调用确切的类型 self
    • 然后,尝试插入匹配所需的多个解引用运算符
  • self对类型使用(call-by-value)T声明的方法就像使用&self(call-by-reference)类型声明它们一样,&T并且对点运算符左侧的引用调用.
  • 上面的规则首先尝试使用原始内置解除引用,如果没有匹配,Deref则使用带有trait 的重载.

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

huo*_*uon 121

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

算法的核心是:

  • 对于每个"解除引用步骤" U(即设置U = T然后U = *T,......)
    1. 如果有一种bar接收器类型(方法中的类型self)U完全匹配的方法,请使用它("按值方法")
    2. 否则,添加一个auto-ref(take &&mutof receiver),如果某个方法的接收器匹配&U,则使用它("autorefd方法")

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

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

这些规则似乎在大多数情况下都是这样做的,尽管能够编写明确的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),所以我们取消引用一次得到U = &&&&X,匹配1(带Self = &&&X)并且调用是RefM::refm(*foo)
  • Z,不匹配任何一步,所以它被解除引用一次,得到Y,也不匹配,所以它再次被取消引用,得到X,它不匹配1,但在自动反射后匹配,所以调用是RefM::refm(&**foo).
  • &&A,1.不匹配,也不匹配2.因为特征没有实现&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,因此错误.
  • &&A1.不匹配,但autorefing给人&&&A,这确实匹配,所以调用的是M::m(&foo)Self = &&&A.

(这个答案基于代码,并且与(稍微过时的)README相当接近.编译器/语言的这一部分的主要作者Niko Matsakis也对这个答案进行了瞥.)

  • 这个答案似乎详尽而详细,但我认为它缺乏简短易懂的规则总结.其中有一个这样的总结[Shepmaster评论](/sf/ask/2549834521/?noredirect=1&lq=1#comment60472747_36426207): "它[deref算法]将尽可能多地deref(`&& String` - >`&String` - >`String` - >`str`),然后在max一次引用(`str` - >`&str`) ". (14认同)
  • 在什么情况下会发生自动解除引用?它是否仅用于方法调用的接收者表达式?也用于现场访问?分配右侧?左手边?函数参数?返回值表达式? (3认同)
  • 注意:目前,nomicon 有一个 TODO 注释,可以从这个答案中窃取信息并将其写在 https://static.rust-lang.org/doc/master/nomicon/dot-operator.html (2认同)

Luk*_*odt 18

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

第一步是构建候选接收器类型列表。通过反复取消引用接收者表达式的类型,将遇到的每个类型添加到列表中,然后最后尝试进行无大小强制转换,如果成功,则添加结果类型来获得这些。然后,对于每个候选者T,在 之后立即将&T和添加&mut T到列表中T

例如,如果接收者有 type Box<[i32;2]>,那么候选类型将是Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2](通过解引用), &[i32; 2], &mut [i32; 2], [i32](通过无大小强制转换), &[i32], 和 finally &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?其中的类型是“获胜”的类型,即可以找到拟合方法的第一种类型。还要记住,列表中的第一个类型始终是接收者表达式的类型。最后,我将列表格式化为三行,但这只是格式化:这个列表是一个平面列表。