ChatGPT解决这个技术问题 Extra ChatGPT

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 == @
}

(Playground)

因此,似乎或多或少:

编译器将根据需要插入尽可能多的取消引用运算符来调用方法。

编译器在解析使用 &self (call-by-reference) 声明的方法时: 首先尝试调用 self 的单个取消引用 然后尝试调用 self 的确切类型 然后尝试插入匹配所需的尽可能多的取消引用运算符

首先尝试调用 self 的单个取消引用

然后尝试调用 self 的确切类型

然后,尝试为匹配插入尽可能多的取消引用运算符

对 T 类型使用 self(按值调用)声明的方法的行为就像它们对类型 &T 使用 &self(按引用调用)声明并调用对点运算符左侧的任何内容的引用。

上述规则首先尝试使用原始内置解引用,如果没有匹配,则使用具有 Deref 特征的重载。

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

我已将其交叉发布到 Rust subreddit,希望能得到一些好的答案!
为了获得更多乐趣,请尝试在仿制药中重复实验并比较结果。

t
toku-sa-n

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

该算法的核心是:

对于每个“取消引用步骤”U(即设置 U = T 然后设置 U = *T,...),如果有一个方法栏,其中接收器类型(方法中的 self 类型)与 U 完全匹配,请使用它(“按值方法”)否则,添加一个自动引用(接收器的 & 或 &mut),并且,如果某个方法的接收器与 &U 匹配,则使用它(“自动引用方法”)

如果有一个方法栏,其中接收者类型(方法中的 self 类型)与 U 完全匹配,请使用它(“按值方法”)

否则,添加一个自动引用(接收器的 & 或 &mut),如果某个方法的接收器与 &U 匹配,则使用它(“自动引用方法”)

值得注意的是,一切都考虑方法的“接收者类型”,不是特征的 Self 类型,即 impl ... for Foo { fn method(&self) {} } 在匹配方法时会考虑 &Foo,而 fn method2(&mut self) 会认为匹配时大约 &mut Foo

如果在内部步骤中有多个有效的 trait 方法是错误的(也就是说,在 1. 或 2. 中的每一个中只能有零个或一个有效的 trait 方法,但每个都可以有一个有效:一个from 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( *富)

Z, 不匹配任何一个步骤,所以它被取消引用一次,得到 Y,它也不匹配,所以它再次被取消引用,得到 X,它不匹配 1,但在 autorefing 之后匹配,所以调用是 RefM::refm(&**foo)。

&&A,1. 不匹配,2. 也不匹配,因为没有为 &A(对于 1)或 &&A(对于 2)实现特征,所以它被取消引用到与 1. 匹配的 &A,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,因此出现错误。

&&A, 1. 不匹配,但 autorefing 给出 &&&A,它确实匹配,所以调用是 M::m(&foo) 与 Self = &&&A。

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


这个答案似乎详尽而详细,但我认为它缺乏简短易懂的规则总结。 comment by Shepmaster 中给出了一个这样的总结:“它 [deref 算法] 将尽可能多次地 deref (&&String -> &String -> String -> str),然后最多引用一次 (str -> &str)”。
(我不知道我自己的解释有多准确和完整。)
在什么情况下会发生自动取消引用?它仅用于方法调用的接收者表达式吗?还用于字段访问?赋值右手边?左手边?函数参数?返回值表达式?
注意:目前,nomicon 有一个 TODO 注释可以从这个答案中窃取信息并将其写在 static.rust-lang.org/doc/master/nomicon/dot-operator.html
强制 (A) 在此之前尝试过,还是 (B) 在此之后尝试过,或者 (C) 在该算法的每个步骤中尝试过,或者 (D) 其他什么?
L
Lukas Kalbertodt

Rust 引用有 a chapter about the method call expression。我复制了下面最重要的部分。提醒:我们讨论的是表达式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,在以下位置搜索具有该类型接收器的可见方法: T 的固有方法(直接在 T [¹] 上实现的方法)。由 T 实现的可见特征提供的任何方法。 [...]

关于 [¹] 的注意事项:我实际上认为这个措辞是错误的。I've opened an issue。让我们忽略括号中的那句话。)

让我们详细浏览您的代码中的几个示例!对于您的示例,我们可以忽略有关“无大小强制”和“固有方法”的部分。

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

创建候选接收器类型列表:i32 不能被取消引用,所以我们已经完成了第 1 步。列表:[i32] 接下来,我们添加 &i32 和 &mut i32。列表:[i32, &i32, &mut i32]

i32 不能被取消引用,所以我们已经完成了第 1 步。列表:[i32]

接下来,我们添加 &i32 和 &mut i32。列表:[i32, &i32, &mut i32]

搜索每个候选接收器类型的方法:我们找到具有接收器类型 i32 的 ::m。所以我们已经完成了。

我们发现 ::m 具有接收器类型 i32。所以我们已经完成了。

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

创建候选接收器类型列表:&&A 可以取消引用到 &A,因此我们将其添加到列表中。 &A 可以再次取消引用,因此我们还将 A 添加到列表中。 A 不能被取消引用,所以我们停止。 List: [&&A, &A, A] 接下来,对于列表中的每个类型 T,我们在 T 之后立即添加 &T 和 &mut T。 List: [&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut 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 as M>::m 确实具有接收器类型 &&&A。所以我们完成了。

没有接收器类型 &&A 的方法,所以我们转到列表中的下一个类型。

我们发现方法 <&&&A as M>::m 确实具有接收器类型 &&&A。所以我们完成了。

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

(*X{val:42}).m() → ::m [⟪i32⟫, &i32, &mut i32]

X{val:42}.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() → ::refm [i32, ⟪&i32⟫, &mut i32]

X{val:42}.refm() → ::refm [X, ⟪&X⟫, &mut X, i32, &i32, &mut i32]

(&X{val:42}).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() → ::refm [Y, &Y, &mut Y, i32, ⟪&i32⟫, &mut i32]

Z{val:Y{val:42}}.refm() → ::refm [Z, &Z, &mut Z, Y, &Y, &mut Y, i32, ⟪&i32⟫, &mut i32]

Am() → ::m [⟪A⟫, &A, &mut A]

(&A).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() → ::refm [A, ⟪&A⟫, &mut A]

(&A).refm() → ::refm [⟪&A⟫, &&A, &mut &A, A, &A, &mut A]

(&&A).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]


J
Jiang

这个问题困扰了我很久,尤其是这部分:

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

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