模式匹配引用时的奇怪类型

Weird type when pattern matching references

我在读这个 的时候遇到了这个奇怪的行为,这个 post 的核心问题是 当你匹配 (&k, &v) = &(&String, &String)k v 将获得类型 String .

为了弄清楚发生了什么,我写了下面的测试代码,结果让我更加震惊和困惑:

Playground Link

fn main() {
    let x: &(&String, &String) = &(&String::new(), &String::new());
    let ref_to_x: &&(&String, &String) = &x;
    let ref_ref_to_x: &&&(&String, &String) = &&x;
    let ref_ref_ref_to_x: &&&&(&String, &String) = &&&x;
    
    // code snippet 1
    let (a, b) = x;                // type of a: &&String, type of b: &&String
    let (a, b) = ref_to_x;         // type of a: &&String, type of b: &&String
    let (a, b) = ref_ref_to_x;     // type of a: &&String, type of b: &&String
    let (a, b) = ref_ref_ref_to_x; // type of a: &&String, type of b: &&String

    // code snippet 2
    let &(a, b) = x;                // type of a: &String, type of b: &String
    let &(a, b) = ref_to_x;        // type of a: &&String, type of b: &&String
    let &(a, b) = ref_ref_to_x;    // type of a: &&String, type of b: &&String
    let &(a, b) = ref_ref_ref_to_x;// type of a: &&String, type of b: &&String

    // code snippet 3
    let (&a, &b) = x;               // type of a: String, type of b: String
    let (&a, &b) = ref_to_x;        // type of a: String, type of b: String
    let (&a, &b) = ref_ref_to_x;    // type of a: String, type of b: String
    let (&a, &b) = ref_ref_ref_to_x;// type of a: String, type of b: String
}

我添加到行尾的ab的类型注释是由rust-analyzer推断的。

注意code snippet 3 不会编译,因为错误can not move out of xx because it's borrowrd/can not move out of xx which is behind a shared reference,但我认为这无关紧要(也许我在这里错了,如果是这样,请指出我,谢谢)因为我们专注于 ab.

的类型

我的问题是:

  1. 为什么 ab 总是具有相同的类型,即使在代码片段 1/2/3 中 RHS 具有不同的类型(x/ref_to_x/ref_ref_to_x..)?
  2. 这种匹配是如何发生的(如果有一个逐步的匹配过程,我们将不胜感激)?
  3. 如何在写代码时得到与rust-analyzer/rustc完全相同的类型推断?

顺便说一句,这与 rfc 2005 match-ergonomics 相关吗?我在谷歌上搜索了很多,发现很多人在他们的回答中提到了这一点。

这称为“解构”。它常用于模式匹配,如 if let Some(val) = option.

本质上,这个:

let x: (&String, &String) = (&String::new(), &String::new());
let (&a, &b) = x; // a: String, b: String

相当于:

let (&a, &b) = (&String::new(), &String::new()); // a: String, b: String

相当于:

let (a, b) = (String::new(), String::new());

为了说明更多,我将使用一个更简单的示例。

let value: String = String::new();
let &also_value = &value; // also_value: String

解构的工作原理是“简化”双方。在这种情况下,它将取消对双方的引用,导致

let also_value = value;

这也可以通过间接方式实现。如果我们在两者之间添加一个步骤:

let value: String = String::new();
let ref_to_value: &String = &value;
let &also_value = ref_to_value; // also_value: String

它可以看穿间接寻址,现在将取消引用 ref_to_value 以再次获得 String

也可能涉及匹配人体工程学,但仅用于取消引用整个元组。

注意:要在解构赋值中获取引用,您可以改用 ref:

let value: String = String::new();
let ref ref_to_value = value; // ref_to_value: &String

一般只用于模式匹配,如:

if let Some(ref x) = option { ... }

是的。您所看到的是符合人体工程学的实际应用,它们的行为可能并非您所期望的那样。

匹配人体工程学的工作方式是使用绑定模式。提供三种绑定模式,即使不匹配人体工程学也可以使用:

  • 搬家。这是引入匹配人体工程学之前的默认绑定模式,它总是尝试移动(或复制)值。
  • ref。这是将 ref 运算符应用于绑定(令人惊讶)时得到的结果,它添加了 one 引用。例如,在match e { ref r => ... }中,r&e
  • ref mut。类似于 ref,但使用可变借用(并使用 ref mut 运算符指定)。

该过程的工作原理如下:编译器从外向内处理模式。该过程以 move 作为绑定模式开始。

每次编译器需要将 non-reference 模式(文字、结构、元组、切片)与引用匹配时,它会自动取消引用引用并更新绑定模式:当 & 引用匹配时,我们将获得 ref 绑定模式,对于 &mut 引用,如果当前绑定模式为 [=],我们将获得 ref 14=] 或其他 ref mut。然后重复此过程,直到我们不再有参考为止。

如果我们匹配引用模式(绑定、通配符、const引用类型或&/&mut patterns),默认绑定模式重置回 move.

绑定变量时,编译器会查看当前绑定模式:对于 move,它将匹配类型 as-is。对于 refref mut,它将分别添加 &&mut但只有一个.

让我们效仿你的例子line-by-line。

let (a, b) = x;                // type of a: &&String, type of b: &&String

我们将 non-reference 模式(元组模式)与引用(&(&String, &String) 类型)进行匹配。所以我们取消引用引用并将绑定模式设置为 ref.

现在我们得到了一个元组模式来匹配 (&String, &String) 类型的元组和 ref 的绑定模式。我们将 a&String 进行匹配:这是一个参考模式(绑定),因此我们不会更改绑定模式。但是,我们已经有了ref的绑定模式。我们匹配的类型是&Stringref表示我们添加了一个引用,所以我们以&&String结束。 b.

也发生了完全相同的事情
let (a, b) = ref_to_x;         // type of a: &&String, type of b: &&String

在这里,就像前面的示例一样,我们将 non-reference 模式(元组模式)与引用 (&&(&String, &String)) 进行匹配。所以我们取消引用并将绑定模式设置为 ref。但是我们还有一个引用:&(&String, &String)。所以我们再次取消引用。绑定模式已经是ref,我们就不用碰了。我们以 (a, b)(&String, &String) 匹配结束。这意味着 a = &Stringb = &String。但请记住我们使用的是 ref 绑定模式,所以我们应该添加一个引用。我们只添加了一个参考,即使我们匹配了两个!最后,我们有a = &&Stringb = &&String .

此代码段中的其余示例以相同的方式工作。

let &(a, b) = ref_to_x;        // type of a: &&String, type of b: &&String

在这里,我们首先将 & 模式与类型 &&(&String, &String) 的引用进行匹配。这将删除两个引用,使我们将 (a, b)&(&String, &String) 进行匹配。从现在开始,我们像第一个例子一样继续。

此代码段中的其余示例类似。

let (&a, &b) = x;               // type of a: String, type of b: String

这是最有趣的一个。还记得我们是如何谈论参考与 non-reference 模式的吗?在这个例子中,事实起着至关重要的作用。

首先我们将元组模式与类型 &(&String, &String) 进行匹配。我们取消引用元组并设置 binding_mode = ref。现在我们匹配元组:我们必须将 &a&b 分别与 &String 匹配,绑定模式设置为 ref.

当我们将 &a&String 匹配时会发生什么?好吧,记住 & 是一个参考模式,在匹配参考模式时我们 完全忽略绑定模式 。所以我们将 &a&String 进行匹配,绑定模式重置为 move。这从两边删除了引用,给我们留下 a = String&b.

相同

此代码段中的下一个示例是相同的。