max_by_key on Map 不允许将元组解构为键值对

max_by_key on Map doesn't allow destructuring of tuple into key-value pair

我正在学习 Rust,并且对所有权、借用和引用的概念相当了解。我已经读到 Rust Book 第二版的第 8 章了。

我正在使用 map as given in an exercise 实现 mode 函数。我使用 Iterator::max_by_key:

编写了以下实现
use std::collections::HashMap;

fn main() {
    let vs = vec![0, 0, 1, 1, 3, 4, 5, 6, 3, 3, 3];

    let mut counts = HashMap::new();
    for num in vs {
        let count = counts.entry(num).or_insert(0);
        *count += 1;
    }

    // This works
    let u = counts.iter().max_by_key(|v| v.1);

    // This doesn't work
    let v = counts.iter().max_by_key(|(k, v)| v);
}

我收到以下编译器错误

error[E0495]: cannot infer an appropriate lifetime for pattern due to conflicting requirements
  --> src/main.rs:16:43
   |
16 |     let v = counts.iter().max_by_key(|(k, v)| v);
   |                                           ^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #2 defined on the body at 16:38...
  --> src/main.rs:16:38
   |
16 |     let v = counts.iter().max_by_key(|(k, v)| v);
   |                                      ^^^^^^^^^^
note: ...so that reference does not outlive borrowed content
  --> src/main.rs:16:43
   |
16 |     let v = counts.iter().max_by_key(|(k, v)| v);
   |                                           ^
note: but, the lifetime must be valid for the method call at 16:13...
  --> src/main.rs:16:13
   |
16 |     let v = counts.iter().max_by_key(|(k, v)| v);
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...so that a type/lifetime parameter is in scope here
  --> src/main.rs:16:13
   |
16 |     let v = counts.iter().max_by_key(|(k, v)| v);
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

这个错误是什么意思,为什么不允许这样做?

更新 1: 解决了我的问题。如果我使用的是稳定的编译器,我就不会问这个问题。在这里我遇到了意外的编译错误,所以我不会将其作为重复项关闭。

简而言之,使用参考(playground)

let v = counts.iter().max_by_key(|&(_, v)| v);

总而言之,第一个示例有效,因为 v 是可复制的,这意味着您将在闭包中获得 v 的副本。 元组不可复制,这意味着元组将被移出 hashmap,这是不允许的,这就是您必须使用引用的原因。

解决方案是添加单个 &:

counts.iter().max_by_key(|&(k, v)| v);
//                        ^

... 或(每晚)添加一个 *:

counts.iter().max_by_key(|(k, v)| *v);
//                                ^

它后面有详细的解释以及如何找到自己的说明。如果你没有时间,最后有总结。


那么为什么这样做有效?

为了找出答案,让我们先分析一下这段代码中 x 的类型(这是您的第一个版本,但为了清楚起见,我将 v 重命名为 x):

counts.iter().max_by_key(|x| x.1);

要检查 x 的类型,我们基本上有两种可能性:挖掘文档或让编译器告诉我们。让我们先深入研究文档,然后用编译器确认这些知识。

所以 counts 是一个 HashMap<{integer}, {integer}>,其中 {integer} 只是某种整数:编译器仍然必须找出确切的整数。如果没有给出更具体的信息(如您的示例),编译器默认为 i32 整数。为了方便我们,让我们修复整数类型:

let mut counts: HashMap<i32, u32> = HashMap::new();

所以现在你写 counts.iter() ...让我们通过查看 the docs:

来检查它做了什么
pub fn iter(&self) -> Iter<K, V>

现在我们可以单击 Iter 获取有关该类型的更多信息,或者我们可以单击左侧的感叹号:

不管怎样,我们看到了这个重要的暗示:

impl<'a, K, V> Iterator for Iter<'a, K, V>
    type Item = (&'a K, &'a V);

这告诉我们 HashMap::iter() 的 return 类型是一个迭代器,它产生 (&K, &V) 类型的项(引用的二元组)。这里,K 是哈希映射的键类型(i32),V 是哈希映射的值类型(u32)。所以我们的迭代器产生 (&i32, &u32).

类型的元素

好的,太棒了!现在我们需要检查 Iterator::max_by_key:

fn max_by_key<B, F>(self, f: F) -> Option<Self::Item> 
where
    B: Ord,
    F: FnMut(&Self::Item) -> B, 

它有点复杂,但别担心!我们看到该方法采用(除了 self)一个参数 f: F。这是你传入的闭包。 where 子句告诉我们 F: FnMut(&Self::Item) 意味着 F 是一个函数对象,它有一个 &Self::Item 类型的参数。

但是我们已经知道迭代器的 Self::Item 是什么:(&i32, &u32)。所以 &Self::Item(加上引用)是 &(&i32, &u32)!这是闭包参数的类型,因此也是 x.

的类型

让我们检查一下我们的研究是否正确。您可以通过强制类型错误轻松地指示编译器告诉您变量 x 的类型。让我们通过添加表达式 x == () 来实现。在这里,我们尝试将您的变量与永远不起作用的 () 进行比较。事实上我们得到了错误:

14 |         x == ();
   |           ^^ can't compare `&(&i32, &u32)` with `()`

成功!我们正确地找到了 x 的类型。那么这对我们有什么帮助?

在第二个例子中,你写道:

counts.iter().max_by_key(|(k, v)| v);

所以你在闭包的参数列表中使用了模式匹配。但是有人可能会想:等等,编译器怎么能将模式 (k, v) 匹配到类型 &(&i32, &u32)?开头有个引用不合适!

这正是稳定编译器上发生的事情:

error[E0658]: non-reference pattern used to match a reference (see issue #42640)
  --> src/main.rs:18:39
   |
18 |     counts.iter().max_by_key(|(k, v)| v);
   |                               ^^^^^^ help: consider using a reference: `&(k, v)`

您可以看到模式 &(k, v) 确实适合 &(&i32, &u32)k = &i32v = &u32)。

所以说到稳定的编译器,你的问题只是你的模式不符合预期的类型。

那么夜间错误是怎么回事?

最近,一些符合人体工程学的改进在 Rust 中落地(仍然只在夜间),这有助于减少常见情况下的嘈杂代码。这种特殊的改进是在 RFC 2005 中提出的。这种常见的情况是在元组的引用上进行匹配,并希望获得对元素的引用,就像在这种情况下,我们在类型 &(bool, String):

上进行匹配
match &(true, "hi".to_string()) {
    // ...
}

因此,如果不考虑引用,人们可能会使用模式 (b, s)(类似于您对 (k, v) 所做的)。但这不起作用(稳定),因为模式不适合(它缺少参考)。

所以模式 &(b, s) 起作用了——至少有点。因为虽然模式与类型匹配,但现在 s 具有类型 String 并且因此试图移出不允许的原始元组(因为我们只有对它的引用)。

所以你写的是:&(b, ref s)。现在 s 的类型 &String 很好。

由于 &ref 对很多人来说似乎很吵,Rust 想让这些情况变得更容易。跳过一些细节,当模式用于引用类型时,Rust 基本上会自动将 (a, b) 之类的模式转换为 &(ref a, ref b) 。同样,这在某些情况下会有所帮助,但也会引入一些意想不到的引用——例如您的示例:

counts.iter().max_by_key(|(k, v)| v);

正如我们所见,模式 (k, v) 实际上不适合该类型,但 Rust 应用规则并将您的模式转换为 &(ref k, ref v)。现在模式匹配工作了,但我们还有另一个问题:

现在v是一个&&u32:引用引用! (要了解为什么会这样,只需仔细检查我们上面讨论的所有类型。)但是内部引用只在迭代器存在时存在,所以我们不能 return 它和 yada yada一生的问题。简单的解决方案就是删除外部引用,因为我们不需要它。

我们通过明确我们的模式(并使其稳定运行)来实现这一点:

counts.iter().max_by_key(|&(k, v)| v);

现在 v 又是 &i32(但是我们引用的 i32 值与哈希映射一样长,所以一切都很好)。或者我们可以通过添加 *:

来移除外部引用
counts.iter().max_by_key(|(k, v)| *v);

这仍然使用夜间人体工程学改进,但删除了外部引用,因此 *v 也是 &i32

您可能会注意到,由于 i32Copy,我们还可以添加两个 *

总结

嗯,这是对问题的深入探讨。简而言之:

  • stable 上,您的模式与类型不兼容((k, v) 不适合 &(&{integer}, &{integer})。因此您可以通过修复你的图案。
  • nightly(使用 RFC 2005 match ergonomics),您被编译器引入的附加参考层咬伤了。这会导致生命周期错误。幸运的是,您不需要这个额外的参考,所以您可以简单地删除它。