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

标签 rust reference language-lawyer

我正在学习/试验 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 的确切类型
  • 然后,尝试为匹配插入尽可能多的解引用运算符
  • 使用 self 声明的方法(按值调用)类型 T表现得好像它们是使用 &self 声明的一样(call-by-reference) 类型 &T并调用对点运算符左侧的任何内容的引用。
  • 上面的规则首先尝试使用原始的内置解引用,如果没有匹配,则重载 Deref特性被使用。

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

    最佳答案

    您的伪代码非常正确。在这个例子中,假设我们有一个方法调用 foo.bar()哪里foo: T .我将使用 fully qualified syntax (FQS) 明确说明调用方法的类型,例如A::bar(foo)A::bar(&***foo) .我只是要写一堆随机的大写字母,每个字母都只是一些任意的类型/特征,除了T始终是原始变量的类型 foo该方法被调用。
    该算法的核心是:

  • 每个"dereference step" U (即先设置 U = T 然后设置 U = *T ,...)
  • 如果有方法 bar其中接收器类型(方法中 self 的类型)匹配 U确切地说,使用它(a "by value method")
  • 否则,添加一个自动引用(取接收器的 &&mut),并且,如果某个方法的接收器匹配 &U , 使用它 ( an "autorefd method" )


  • 值得注意的是,一切都考虑了方法的“接收器类型”,而不是 Self特征的类型,即 impl ... for Foo { fn method(&self) {} }想想&Foo匹配方法时,和fn method2(&mut self)会考虑&mut Foo匹配时。
    如果在内部步骤中有多个有效的 trait 方法(也就是说,在 1. 或 2. 中的每一个中只能有零个或一个有效的 trait 方法,但可以有一个对每个有效的 trait 方法:一个from 1 将首先被采用),并且固有方法优先于特征方法。如果我们在没有找到任何匹配的情况下到达循环的末尾,这也是一个错误。有递归Deref也是错误的实现,这使循环无限(它们将达到“递归限制”)。
    在大多数情况下,这些规则似乎是我的意思,尽管能够编写明确的 FQS 形式在某些边缘情况下非常有用,并且对于宏生成的代码的合理错误消息。
    只添加了一个自动引用,因为
  • 如果没有限制,事情会变得糟糕/缓慢,因为每种类型都可以有任意数量的引用
  • 取一引用&foofoo 保持密切联系(它是 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. 也不匹配,因为该 trait 没有为 &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按值(value),因此移出 foo ,因此错误。
  • &&A , 1. 不匹配,但自动引用给出 &&&A ,它确实匹配,所以调用是 M::m(&foo)Self = &&&A .

  • (此答案基于 the codeis reasonably close to the (slightly outdated) README 。编译器/语言这部分的主要作者 Niko Matsakis 也浏览了此答案。)

    关于rust - Rust 的确切自动解引用规则是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28519997/

    相关文章:

    file - 如何从文件中获取随机行?

    C++ 对非 Const 对象的 Const 引用与对非 Const 对象的非 Const 引用之间的区别

    c++ - 在 C++11 基于范围的 'for' 循环中获取对 STL 容器元素的引用

    http - 201 的响应实体类型已创建?

    c++ - 使用 C++20 using-enum-declaration 进行二义性名称查找

    c++ - 是否有任何 C 或 C++ 标准识别内存映射文件的存在?

    testing - 如何让 Cargo Test 漂亮地打印失败输出

    c - 如何使用 FFI 从 C 调用 Rust 结构的方法?

    rust - 从空元组创建不带参数的变体

    c - c中struct中的多维数组引用