我正在学习/试验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 == @
}

(游乐场)

所以,看起来,或多或少:

The compiler will insert as many dereference operators as necessary to invoke a method. The compiler, when resolving methods declared using &self (call-by-reference): First tries calling for a single dereference of self Then tries calling for the exact type of self Then, tries inserting as many dereference operators as necessary for a match Methods declared using self (call-by-value) for type T behave as if they were declared using &self (call-by-reference) for type &T and called on the reference to whatever is on the left side of the dot operator. The above rules are first tried with raw built-in dereferencing, and if there's no match, the overload with Deref trait is used.

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


当前回答

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

算法的核心是:

对于每个“解引用步骤”U(即设置U = T,然后U = *T,…) 如果有一个方法条,其中接收者类型(方法中的self类型)与U完全匹配,则使用它(“按值方法”) 否则,添加一个auto-ref (take & or &mut of receiver),如果某个方法的接收者匹配& u,则使用它(一个“autorefd方法”)

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

It is an error if there's ever multiple trait methods valid in the inner steps (that is, there can be only be zero or one trait methods valid in each of 1. or 2., but there can be one valid for each: the one from 1 will be taken first), and inherent methods take precedence over trait ones. It's also an error if we get to the end of the loop without finding anything that matches. It is also an error to have recursive Deref implementations, which make the loop infinite (they'll hit the "recursion limit").

在大多数情况下,这些规则似乎都是按照我的意思来做的,尽管能够编写明确的FQS表单在某些边缘情况下非常有用,对于宏生成代码的合理错误消息也非常有用。

只添加了一个自动引用,因为

如果没有约束,事情就会变得糟糕/缓慢,因为每种类型都可以有任意数量的引用 接受一个引用&foo会保留对foo的强连接(它是foo本身的地址),但接受更多引用会开始失去它:&&foo是堆栈上存储&foo的某个临时变量的地址。

例子

假设我们有一个foo.refm()调用,如果foo有type:

X, then we start with U = X, refm has receiver type &..., so step 1 doesn't match, taking an auto-ref gives us &X, and this does match (with Self = X), so the call is RefM::refm(&foo) &X, starts with U = &X, which matches &self in the first step (with Self = X), and so the call is RefM::refm(foo) &&&&&X, this doesn't match either step (the trait isn't implemented for &&&&X or &&&&&X), so we dereference once to get U = &&&&X, which matches 1 (with Self = &&&X) and the call is RefM::refm(*foo) Z, doesn't match either step so it is dereferenced once, to get Y, which also doesn't match, so it's dereferenced again, to get X, which doesn't match 1, but does match after autorefing, so the call is RefM::refm(&**foo). &&A, the 1. doesn't match and neither does 2. since the trait is not implemented for &A (for 1) or &&A (for 2), so it is dereferenced to &A, which matches 1., with Self = A

假设我们有foo.m(), A不是Copy,如果foo有type:

A,那么U = A直接匹配self,因此调用self = A的M:: M (foo) 和,然后是1。不匹配,2也不匹配。(&A和&&A都没有实现trait),所以它被解引用到A, A确实匹配,但是M:: M (*foo)需要按值取A,因此移出foo,因此出现错误。 一个1。不匹配,但autorefing给出&&&A,它匹配,所以调用M:: M (&foo) with Self = &&&A。

(这个答案是基于代码的,并且相当接近(略过时)README。这部分编译器/语言的主要作者Niko Matsakis也浏览了一下这个答案。)

其他回答

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

The first step is to build a list of candidate receiver types. Obtain these by repeatedly dereferencing the receiver expression's type, adding each type encountered to the list, then finally attempting an unsized coercion at the end, and adding the result type if that is successful. Then, for each candidate T, add &T and &mut T to the list immediately after T. For instance, if the receiver has type Box<[i32;2]>, then the candidate types will be Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2] (by dereferencing), &[i32; 2], &mut [i32; 2], [i32] (by unsized coercion), &[i32], and finally &mut [i32]. Then, for each candidate type T, search for a visible method with a receiver of that type in the following places: T's inherent methods (methods implemented directly on T [¹]). Any of the methods provided by a visible trait implemented by T. [...]

(关于[¹]的注意事项:实际上我认为这种措辞是错误的。我开了一期。让我们忽略括号里的那句话。)


让我们详细地看一下代码中的几个示例!对于您的示例,我们可以忽略关于“unsized强制”和“固有方法”的部分。

(*X{val:42}).m():接收方表达式的类型是i32。我们执行以下步骤:

创建候选接收者类型列表: I32不能被解除引用,所以我们已经完成了第1步。列表(手机等): 接下来,我们添加&i32和&mut i32。列表:[i32, &i32, &mut i32] 搜索每个候选接收器类型的方法: 我们发现<i32为M>:: M,其接收类型为i32。我们已经做完了。

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

Creating list of candidate receiver types: &&A can be dereferenced to &A, so we add that to the list. &A can be dereferenced again, so we also add A to the list. A cannot be dereferenced, so we stop. List: [&&A, &A, A] Next, for each type T in the list, we add &T and &mut T immediately after T. List: [&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A] Searching for methods for each candidate receiver type: There is no method with receiver type &&A, so we go to the next type in the list. We find the method <&&&A as M>::m which indeed has the receiver type &&&A. So we are done.


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

(*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, &Muth 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]

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

算法的核心是:

对于每个“解引用步骤”U(即设置U = T,然后U = *T,…) 如果有一个方法条,其中接收者类型(方法中的self类型)与U完全匹配,则使用它(“按值方法”) 否则,添加一个auto-ref (take & or &mut of receiver),如果某个方法的接收者匹配& u,则使用它(一个“autorefd方法”)

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

It is an error if there's ever multiple trait methods valid in the inner steps (that is, there can be only be zero or one trait methods valid in each of 1. or 2., but there can be one valid for each: the one from 1 will be taken first), and inherent methods take precedence over trait ones. It's also an error if we get to the end of the loop without finding anything that matches. It is also an error to have recursive Deref implementations, which make the loop infinite (they'll hit the "recursion limit").

在大多数情况下,这些规则似乎都是按照我的意思来做的,尽管能够编写明确的FQS表单在某些边缘情况下非常有用,对于宏生成代码的合理错误消息也非常有用。

只添加了一个自动引用,因为

如果没有约束,事情就会变得糟糕/缓慢,因为每种类型都可以有任意数量的引用 接受一个引用&foo会保留对foo的强连接(它是foo本身的地址),但接受更多引用会开始失去它:&&foo是堆栈上存储&foo的某个临时变量的地址。

例子

假设我们有一个foo.refm()调用,如果foo有type:

X, then we start with U = X, refm has receiver type &..., so step 1 doesn't match, taking an auto-ref gives us &X, and this does match (with Self = X), so the call is RefM::refm(&foo) &X, starts with U = &X, which matches &self in the first step (with Self = X), and so the call is RefM::refm(foo) &&&&&X, this doesn't match either step (the trait isn't implemented for &&&&X or &&&&&X), so we dereference once to get U = &&&&X, which matches 1 (with Self = &&&X) and the call is RefM::refm(*foo) Z, doesn't match either step so it is dereferenced once, to get Y, which also doesn't match, so it's dereferenced again, to get X, which doesn't match 1, but does match after autorefing, so the call is RefM::refm(&**foo). &&A, the 1. doesn't match and neither does 2. since the trait is not implemented for &A (for 1) or &&A (for 2), so it is dereferenced to &A, which matches 1., with Self = A

假设我们有foo.m(), A不是Copy,如果foo有type:

A,那么U = A直接匹配self,因此调用self = A的M:: M (foo) 和,然后是1。不匹配,2也不匹配。(&A和&&A都没有实现trait),所以它被解引用到A, A确实匹配,但是M:: M (*foo)需要按值取A,因此移出foo,因此出现错误。 一个1。不匹配,但autorefing给出&&&A,它匹配,所以调用M:: M (&foo) with Self = &&&A。

(这个答案是基于代码的,并且相当接近(略过时)README。这部分编译器/语言的主要作者Niko Matsakis也浏览了一下这个答案。)

对于类型T,使用self(按值调用)声明的方法表现为 它们是使用&self(引用调用)来声明&T类型和 在点左边的引用上调用 操作符。

它们的行为并不完全相同。使用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 ();

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

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

如果没有找到匹配的函数签名,则执行自动引用或一些自动deref。