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

8dtrkrch  于 2023-01-09  发布在  其他
关注(0)|答案(4)|浏览(225)

我正在学习/试验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
所以,看起来,或多或少:

  • 编译器将根据调用方法的需要插入尽可能多的解引用运算符。
  • 编译器在解析使用&self(按引用调用)声明的方法时:
  • 首先尝试调用self的单个取消引用
  • 然后尝试调用self的确切类型
  • 然后,尝试插入匹配所需数量的取消引用运算符
  • 对于T类型,使用self(按值调用)声明的方法的行为就好像它们是使用&T类型的&self(按引用调用)声明的,并且在点运算符左侧的任何内容的引用上调用。
  • 首先使用raw内置解引用来尝试上述规则,如果不匹配,则使用带有Deref trait的重载。

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

jxct1oxe

jxct1oxe1#

你的伪代码非常正确,对于这个例子,假设我们有一个方法调用foo.bar(),其中foo: T。(FQS)来明确调用方法的类型,例如A::bar(foo)A::bar(&***foo)。我将编写一堆随机的大写字母,每一个都是一些任意的类型/特性,除了T始终是调用该方法的原始变量foo的类型。
算法的核心是:
1.对于每个"解引用步骤" U(即设置U = T,然后设置U = *T,...)
1.如果有一个方法bar,其中接收器类型(方法中self的类型)与U完全匹配,则使用它("按值方法")
1.否则,添加一个auto-ref(取接收方的&&mut),如果某个方法的接收方与&U匹配,则使用它(一个"autorefd方法")
值得注意的是,每件事都考虑方法的"接收者类型",* 而不是 * trait的Self类型,即impl ... for Foo { fn method(&self) {} }在匹配方法时考虑&Foo,而fn method2(&mut self)在匹配时考虑&mut Foo
如果内部步骤中有多个trait方法有效(也就是说,1.或2.中的每一个只能有零个或一个trait方法有效,但每一个都可以有一个trait方法有效),这是一个错误:1中的一个将首先被采用),并且固有方法优先于trait方法。如果我们到达循环的末尾而没有找到任何匹配,这也是一个错误。递归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实现trait),因此我们解引用一次以获得U = &&&&X,它匹配1(与Self = &&&X匹配),调用为RefM::refm(*foo)
  • Z,不匹配任何一个步骤,所以它被解引用一次,以获得Y,它也不匹配,所以它被再次解引用,以获得X,它不匹配1,但在自动引用后匹配,所以调用是RefM::refm(&**foo)
  • &&A,1.不匹配,2.也不匹配,因为没有为&A(对于1)或&&A(对于2)实现trait,所以它被解引用到&A,它将1.与Self = A匹配

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

  • A,则U = Aself直接匹配,因此调用是M::m(foo)Self = A
  • &A,那么1.不匹配,2.也不匹配(&A&&A都没有实现trait),所以它被解引用到AA匹配,但是M::m(*foo)需要通过值获取A,因此移出foo,因此产生错误。
  • &&A,1.不匹配,但是自动引用给出了&&&A,它确实匹配,所以调用是M::m(&foo)Self = &&&A

(This答案是基于the code的,并且相当接近(稍微过时的)自述文件。编译器/语言这一部分的主要作者Niko Matsakis也浏览了这个答案。)

o2g1uqev

o2g1uqev2#

Rust的引用有a chapter about the method call expression。我复制了下面最重要的部分。提醒:我们讨论表达式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 [¹]上实现的方法)。
    1.由T实现的可见特征提供的任何方法。[...]
    (* 关于[¹]* 的注解:实际上,我认为这个措辞是错误的。I've opened an issue。让我们忽略括号中的句子。)
    让我们从你的代码中详细地看几个例子!对于你的例子,我们可以忽略关于"无大小强制"和"固有方法"的部分。
    • 一米二十分一x**:receiver表达式的类型为i32。我们执行以下步骤:
  • 正在创建候选接收器类型列表:
  • i32无法取消引用,因此我们已经完成了步骤1。* * 一米二三米一x**
  • 接下来,我们添加&i32&mut i32。列表:* * 一米二十六分一x**
  • 为每个候选接收器类型搜索方法:
  • 我们找到了接收器类型为i32<i32 as M>::m,所以我们已经完成了。

到目前为止都很简单。现在让我们选择一个更难的例子:* * (&&A).m()**。接收方表达式的类型为&&A。我们将执行以下步骤:

  • 正在创建候选接收器类型列表:
  • &&A可以被解引用为&A,因此我们将其添加到列表中。&A可以再次被解引用,因此我们也将A添加到列表中。A不能被解引用,因此我们停止。* * 一米三十六分一x**
  • 接下来,对于列表中的每个类型T,我们在T之后立即添加&T&mut T。* * 一米四一米一**
  • 为每个候选接收器类型搜索方法:
  • 没有接收器类型为&&A的方法,所以我们转到列表中的下一个类型。
  • 我们找到了方法<&&&A as M>::m,它确实具有接收器类型&&&A

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

      • 一米四六十一x**→一米四七十一x
      • 一米四八奈一x**→一米四九奈一x
      • x一米五十一个一x**→ x一米五十一个一x x一a2b1 x
      • 一米五二氮一x**→一米五三氮一x一a3b1 x
      • x一米五十四奈一x**→ x一米五十五奈一x x一a四十四b一x
      • x一米五六氮一x**→ x一米五七氮一x
      • 一米五八氮一x**→一米五九氮一x一a6b1 x
      • x一米六一纳米一x**→ x一米六一纳米一x
      • 一米六二氮一x**→一米六三氮一x
      • 一米六四一x**→一米六五一x
      • 一米六六氮一x**→一米六七氮一x
      • 一米六八氮一x**→一米六九氮一x
      • 一米七零一x**→一米七零一x一a12b1x
      • x一米七二氮一x**→ x一米七三氮一x x一铝十三硼一x
      • x一米七四奈一x**→ x一米七五奈一x x
      • x一米七六氮一x**→ x一米七七氮一x
      • x一米七八奈一x**→ x一米七九奈一x x
      • x一米80纳米1 x**→ x一米81纳米1 x x x一铝17硼1 x
      • x一米八二纳米一x**→ x一米八三纳米一x x
      • 一米八四奈一x**→一米八五奈一x一a19b1 x
      • x一米八六氮一x**→ x一米八七氮一x x一a20b1 x
      • 一米八八一x**→一米八八九一x一a21b1x
      • x一米九零纳米一x**→ x一米九零纳米一x x一a22b1 x
      • x一米九二氮一x**→ x一米九三氮一x x一a23b1 x
jgwigjjp

jgwigjjp3#

这个问题困扰了我很长时间,尤其是这一部分:

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

因此,在编写代码时:
第一个月
该函数
fn refm(&X ) { println!("X::refm()");
将调用,因为参数类型为&X
如果没有找到匹配的函数签名,则执行auto-ref或某些auto-deref。

j13ufse2

j13ufse24#

对于类型T使用self(按值调用)声明的方法,其行为就好像它们是使用类型&T的&self(按引用调用)声明的,并且在点操作符左侧的任何引用上调用。
它们的行为并不完全相同。当使用self时,会发生移动(除非结构体是Copy)

let example = X { val: 42};
example.m (); // is the same as M::m (example);
// Not possible: value used here after move
// example.m ();

let example = X { val: 42};
example.refm ();
example.refm ();

相关问题