在不克隆的情况下在闭包内使用 Vector

Consume Vector inside closure without cloning

我有这个数据结构。

let bucket = HashMap<&str, Vec<&str>>

给定

let cluster = Vec<&str>

我想从 Bucket 上的 Vec 扩展它,我可以保证我只会访问每个键值对一次和 [=17] 中的 &str =] 始终是 bucket.

中的键
use std::collections::HashMap;

fn main() {
    let mut bucket: HashMap<&str, Vec<&str>> = HashMap::new();
    bucket.insert("a", vec!["hello", "good morning"]);
    bucket.insert("b", vec!["bye", "ciao"]);
    bucket.insert("c", vec!["good"]);
    let cluster = vec!["a", "b"];
    let cluster2 = vec!["c"];
    let mut clusters = [cluster, cluster2];
    clusters.iter_mut().for_each(|cluster| {
        // I don't like this clone
        let tmp = cluster.clone();
        let tmp = tmp.iter().flat_map(|seq| bucket[seq].
            clone() // I really don't like this other clone
        );
        cluster.extend(tmp);
    });
    println!("{:?}", clusters);
}

这可以编译,但我真正想做的是耗尽 bucket 上的向量,因为我知道我不会再次访问它。

let tmp = tmp.iter().flat_map(|seq| bucket.get_mut(seq).
    unwrap().drain(..)
);

这给了我一个编译器错误:

error: captured variable cannot escape `FnMut` closure body
  --> src/main.rs:13:45
   |
4  |       let mut bucket: HashMap<&str, Vec<&str>> = HashMap::new();
   |           ---------- variable defined here
...
13 |           let tmp = tmp.iter().flat_map(|seq| bucket.get_mut(seq).
   |                                             - ^-----
   |                                             | |
   |  ___________________________________________|_variable captured here
   | |                                           |
   | |                                           inferred to be a `FnMut` closure
14 | |             unwrap().drain(..)
   | |______________________________^ returns a reference to a captured variable which escapes the closure body
   |
   = note: `FnMut` closures only have access to their captured variables while they are executing...
   = note: ...therefore, they cannot allow references to captured variables to escape

我需要去不安全的地方吗?如何?更重要的是,删除 clone 是否合理?

您可以使用 std::mem::take():

消除 bucket[seq].clone()
let tmp = tmp.iter().flat_map(
    |seq| std::mem::take(bucket.get_mut(seq).unwrap()),
);

这将转移现有 Vec 的所有权并在哈希映射中留下一个空的。由于地图保持明确定义的状态,因此这是 100% 安全的。由于空向量不分配,所以它也是高效的。最后,由于您可以保证您不再访问该密钥,所以它是正确的。 (Playground.)

正如评论中所指出的,另一种方法是从哈希映射中删除向量,这也会转移向量的所有权:

let tmp = tmp.iter().flat_map(|seq| bucket.remove(seq).unwrap());

外面的cluster.clone()不能用take()代替,因为你需要旧的内容。这里的问题是您不能扩展您正在迭代的向量,Rust 不允许这样做以实现高效的基于指针的迭代。这里一个简单有效的解决方案是使用索引而不是迭代 (playground):

clusters.iter_mut().for_each(|cluster| {
    let initial_len = cluster.len();
    for ind in 0..initial_len {
        let seq = cluster[ind];
        cluster.extend(std::mem::take(bucket.get_mut(seq).unwrap()));
    }
});

当然,使用索引你要付出间接和绑定检查的代价,但是 rustc/llvm 非常擅长在安全的情况下删除这两者,即使不安全,索引访问可能仍然比克隆更有效。确定这是否改进了您的原始代码的唯一方法是在生产数据上对两个版本进行基准测试。