【问题标题】:How to iterate through a Hashmap, print the key/value and remove the value in Rust?如何遍历 Hashmap、打印键/值并删除 Rust 中的值?
【发布时间】:2018-01-25 05:50:45
【问题描述】:

这在任何语言中都应该是一项微不足道的任务。这在 Rust 中不起作用。

use std::collections::HashMap;

fn do_it(map: &mut HashMap<String, String>) {
    for (key, value) in map {
        println!("{} / {}", key, value);
        map.remove(key);
    }
}

fn main() {}

这是编译器错误:

error[E0382]: use of moved value: `*map`
 --> src/main.rs:6:9
  |
4 |     for (key, value) in map {
  |                         --- value moved here
5 |         println!("{} / {}", key, value);
6 |         map.remove(key);
  |         ^^^ value used here after move
  |
  = note: move occurs because `map` has type `&mut std::collections::HashMap<std::string::String, std::string::String>`, which does not implement the `Copy` trait

为什么它试图移动一个引用?从文档中,我不认为移动/借用适用于引用。

【问题讨论】:

    标签: hashmap rust iteration mutability


    【解决方案1】:

    Rust 实际上支持大量潜在的解决方案来解决这个问题,虽然我自己也发现这种情况一开始有点令人困惑,而且每次我需要对我的哈希图进行更复杂的处理。

    • 要在删除项目时遍历项目,请使用.drain().drain() 具有获取/拥有而不是借用价值的优势。
    • 如果您只想有条件地删除其中一些,请使用.drain_filter()
    • 如果您需要改变每个项目但只想删除其中的一些,您可以在 .drain_filter() 的闭包参数中改变它们,但这将在更改后检查是否删除
    • 如果您需要在更改之前检查是否删除,请使用一个变量来存储检查结果,然后在最后返回该变量。一个稍微慢一些但可能更清晰的替代方法是在一个 for 循环中改变它们,然后在另一个 for 循环或映射中 .drain_filter() 它们。
    • 您也可以通过不在函数参数中借用哈希图来简单地将哈希图放在函数末尾,并在需要时初始化一个新哈希图。显然,这完全删除了哈希图。显然,您可能希望保留哈希图,以免一遍又一遍地重新初始化它。
    • 您也可以调用 .clear() 来删除所有元素,在您完成对它们的迭代以打印它们之后。

    【讨论】:

      【解决方案2】:

      这在任何语言中都应该是一项微不足道的任务。

      Rust 会阻止您在迭代地图时更改地图。在大多数语言中,这是允许的,但通常行为没有明确定义,并且删除项目可能会干扰迭代,从而损害其正确性。

      为什么它试图移动一个引用?

      HashMap 实现IntoIteratorso your loop is equivalent to

      for (key, value) in map.into_iter() {
          println!("{} / {}", key, value);
          map.remove(key);
      }
      

      如果您查看definition of into_iter,您会发现它需要self,而不是&amp;self&amp;mut self。你的变量map 是一个可变引用,IntoIterator 是为&amp;mut HashMap 实现的——into_iter 中的self&amp;mut HashMap,而不是HashMap。可变引用不能被复制(因为任何数据一次只能存在一个可变引用)所以这个可变引用被移动了。

      API 是故意以这种方式构建的,因此您在遍历结构时不会做任何危险的事情。循环完成后,结构的所有权将被放弃,您可以再次使用它。

      一种解决方案是在 Vec 中跟踪您打算删除的项目,然后再删除它们:

      fn do_it(map: &mut HashMap<String, String>) {
          let mut to_remove = Vec::new();
          for (key, value) in &*map {
              if key.starts_with("A") {
                  to_remove.push(key.to_owned());
              }
          }
          for key in to_remove.iter() {
              map.remove(key);
          }
      }
      

      您还可以使用迭代器将地图过滤为新地图。也许是这样的:

      fn do_it(map: &mut HashMap<String, String>) {
          *map = map.into_iter().filter_map(|(key, value)| {
              if key.starts_with("A") {
                  None
              } else {
                  Some((key.to_owned(), value.to_owned()))
              }
          }).collect();
      }
      

      但我刚刚看到 Shepmaster 的编辑 - 我忘记了 retain,这更好。它更简洁,不会像我那样做不必要的复制。

      【讨论】:

      • "通常行为没有明确定义,删除项目会干扰迭代,损害其正确性。"说得好。这种类型的东西引起的错误非常奇怪,之前在 C++ 中已经花了几十个小时来追踪它们。特别是在嵌套循环内。谢谢。
      【解决方案3】:

      至少有两个原因不允许这样做:

      1. 您需要有两个对map 的并发可变引用——一个由for 循环中使用的迭代器持有,一个在变量map 中以调用map.remove

      2. 在尝试改变地图时,您在地图中引用了键和值。如果允许您以任何方式修改映射,这些引用可能会失效,从而为内存不安全打开大门。

      Rust 的核心原则是别名异或可变性。一个值可以有多个不可变引用,也可以有一个可变引用。

      我不认为移动/借用适用于引用。

      每种类型都受制于 Rust 的移动规则以及可变别名。请让我们知道文档的哪一部分说不是这样,以便我们解决这个问题。

      为什么它试图移动一个引用?

      这由两部分组成:

      1. 您只能有一个可变引用,因此可变引用不会实现 Copy 特征
      2. for 循环 take the value to iterate over by value

      当您调用for (k, v) in map {} 时,map 的所有权被转移到 for 循环中,现在消失了。


      我会执行地图的不可变借用 (&amp;*map) 并对其进行迭代。最后,我会清除整个事情:

      fn do_it(map: &mut HashMap<String, String>) {
          for (key, value) in &*map {
              println!("{} / {}", key, value);
          }
          map.clear();
      }
      

      使用以字母“A”开头的键删除每个值

      我会使用HashMap::retain:

      fn do_it(map: &mut HashMap<String, String>) {
          map.retain(|key, value| {
              println!("{} / {}", key, value);
      
              !key.starts_with("a")
          })
      }
      

      这保证了keyvalue 在实际修改地图时不再存在,因此他们本来应该拥有的任何借用现在都消失了。

      【讨论】:

      • 我可以用for (key, value) in map {}; for (key, value) in map {} 得到同样的错误,我认为这个答案不能解释这一点。
      • 一种思考方式是,如果您在循环中调用 map.clear() 会发生什么? keyvalue 是引用,它们不再引用任何东西。 clearremove 都使用 &amp;mut self,从借用检查器的角度来看,它们是相同的。
      • 这让我遇到了一个更奇怪的问题,但我怀疑方法调用语法掩盖了这个问题。 play.rust-lang.org/…
      • @JoshLee 这不是很明显!您正在寻找的神奇关键字将是“rebo​​rrowing”,这是可变引用的一个特殊属性。
      • @vasilakisfil 你不能,很容易。蛮力的答案是为每个谓词启动一个执行程序(How do I synchronously return a value calculated in an asynchronous Future in stable Rust?)。这可能是一个不理想的想法。相反,我可能会尝试将哈希图转换为迭代器,然后转换为流,然后使用StreamExt 过滤流,然后将其转换回哈希图。基准测试很重要。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-10-05
      • 2021-08-10
      • 1970-01-01
      • 2021-12-16
      • 2018-11-21
      相关资源
      最近更新 更多