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

336

我正在学习和尝试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 (按引用调用)声明的,并在点操作符左侧的引用上调用。
  • 以上规则首先尝试使用原始的内置解引用,如果没有匹配,则使用具有 Deref 特性的重载。

什么是准确的自动解引用规则?有人能给出任何形式的设计决策理由吗?


4
我已经将这个帖子在Rust subreddit上发布,希望能得到一些好的回答! - Shepmaster
为了更有趣,尝试在泛型中重复实验并比较结果。 - user2665887
4个回答

230
你的伪代码基本正确。在这个例子中,假设我们有一个方法调用 foo.bar() 其中 foo:T。我将使用完全限定语法 (FQS)明确调用该方法的类型是什么,例如 A::bar(foo)A::bar(&***foo)。我只会写一些随机大写字母,每个字母都是一种任意的类型/特质,除了T始终是原始变量 foo 的类型,该方法在其上调用。

算法的核心是:

  • 对于每个“解引用步骤”U(即设置 U = T 然后 U = *T,...)
    1. 如果存在一个方法 bar,其中接收器类型(方法中的self类型)与 U 完全匹配,则使用它(一个按值方法
    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 的一些临时变量的地址。
示例
假设我们有一个调用 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(对于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,虽然A匹配,但M::m(*foo)需要通过值来获取A,因此移动出foo,导致错误。
  • &&A,1.不匹配,但自动引用给出&&&A,它匹配,因此调用是M::m(&foo),其中Self = &&&A

(本答案基于代码,并且与(稍微过时的)自述文件相当接近。 Niko Matsakis,编译器/语言的主要作者,也浏览了本答案。)


79
这个答案看起来非常详细,但我认为它缺少简短且易于理解的规则总结。可以在Shepmaster的此评论中找到一个这样的总结:“它 [deref算法]会尽可能地进行多次解引用 (&&String -> &String -> String -> str),然后最多只能引用一次 (str -> &str)”。 - Lii
我不知道那个解释的准确性和完整性。 - Lii
7
自动解引用会在哪些情况下发生?它仅用于方法调用的接收器表达式吗?还是对字段访问也适用?赋值右侧?左侧?函数参数?返回值表达式? - Lii
4
注意:目前,nomicon 上有一个 TODO 注释,要从这个答案中窃取信息并将其写入 https://static.rust-lang.org/doc/master/nomicon/dot-operator.html。 - SamB
1
强制转换(A)是在此之前尝试的还是在此之后尝试的(B),还是在此算法的每个步骤中尝试(C),或者(D)其他情况? - haslersn
显示剩余3条评论

35
Rust参考文档中有关于方法调用表达式的章节。下面是其中最重要的部分。提醒一下:我们正在讨论一个表达式recv.m(),其中recv在下文中被称为“接收器表达式”。

第一步是构建候选接收器类型列表。通过反复引用接收器表达式的类型,将遇到的每个类型添加到列表中,然后最终尝试进行不定大小强制转换,在成功时添加结果类型。然后,对于每个候选的T,立即在T之后添加&T&mut 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 无法被解引用,所以我们已经完成了第一步。列表:[i32]
    • 接下来,我们添加 &i32&mut i32。列表:[i32, &i32, &mut i32]
  • 搜索每个候选接收器类型的方法:
    • 我们找到了具有接收器类型 i32<i32 as M>::m 方法。所以我们已经完成了。


到目前为止都很简单。现在让我们选择一个更困难的例子:(&&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 的方法 <&&&A as M>::m。所以我们已经完成了。

以下是您所有示例的候选接收器列表。用 ⟪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]
    

2

我对这个问题困扰了很长时间,特别是对于这一部分:

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

如果找不到匹配的函数签名,则会执行自动引用或自动解引用。


0
使用self声明的类型为T的方法,其行为就像是使用&self声明类型为&T的方法,并在点运算符左侧调用引用的内容。它们并不完全相同。当使用self时,会发生一次移动(除非该结构体是可复制的)。
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 ();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接