从同一个HashMap中借用两个可变值

11

我有以下代码:

use std::collections::{HashMap, HashSet};

fn populate_connections(
    start: i32,
    num: i32,
    conns: &mut HashMap<i32, HashSet<i32>>,
    ancs: &mut HashSet<i32>,
) {
    let mut orig_conns = conns.get_mut(&start).unwrap();
    let pipes = conns.get(&num).unwrap();

    for pipe in pipes.iter() {
        if !ancs.contains(pipe) && !orig_conns.contains(pipe) {
            ancs.insert(*pipe);
            orig_conns.insert(*pipe);
            populate_connections(start, num, conns, ancs);
        }
    }
}

fn main() {}

逻辑并不是很重要,我正在尝试创建一个函数,它将自身传递并在管道上行走。

我的问题是,这段代码无法编译:

error[E0502]: cannot borrow `*conns` as immutable because it is also borrowed as mutable
  --> src/main.rs:10:17
   |
9  |     let mut orig_conns = conns.get_mut(&start).unwrap();
   |                          ----- mutable borrow occurs here
10 |     let pipes = conns.get(&num).unwrap();
   |                 ^^^^^ immutable borrow occurs here
...
19 | }
   | - mutable borrow ends here

error[E0499]: cannot borrow `*conns` as mutable more than once at a time
  --> src/main.rs:16:46
   |
9  |     let mut orig_conns = conns.get_mut(&start).unwrap();
   |                          ----- first mutable borrow occurs here
...
16 |             populate_connections(start, num, conns, ancs);
   |                                              ^^^^^ second mutable borrow occurs here
...
19 | }
   | - first borrow ends here

我不知道如何使它工作。一开始,我试图将两个 HashSet 存储在 HashMap orig_conns pipes )中。

Rust不允许我同时拥有可变和不可变变量。我有点困惑,因为这将是完全不同的对象,但我想如果&start == &num,那么我将有两个对同一对象的不同引用(一个可变,一个不可变)。

没关系,但我该怎么做呢?我要遍历一个 HashSet ,并读取和修改另一个 HashSet 。假设它们不会是相同的 HashSet


还有一个有趣的项目是multi_mut crate,它提供了get_mut_pair()和其他几种方法,可以让哈希表拥有多个可变引用。 - ANimator120
2个回答

11

如果您可以更改数据类型和函数签名,您可以使用RefCell创建内部可变性

use std::cell::RefCell;
use std::collections::{HashMap, HashSet};

fn populate_connections(
    start: i32,
    num: i32,
    conns: &HashMap<i32, RefCell<HashSet<i32>>>,
    ancs: &mut HashSet<i32>,
) {
    let mut orig_conns = conns.get(&start).unwrap().borrow_mut();
    let pipes = conns.get(&num).unwrap().borrow();

    for pipe in pipes.iter() {
        if !ancs.contains(pipe) && !orig_conns.contains(pipe) {
            ancs.insert(*pipe);
            orig_conns.insert(*pipe);
            populate_connections(start, num, conns, ancs);
        }
    }
}

fn main() {}
请注意如果 start == num,线程将会发生 panic,因为这是试图对同一个 HashSet 进行可变和不可变访问的尝试。 < h3 >安全替代 RefCell 根据您确切的数据和代码需求,您还可以使用诸如Cell或其中一个 原子类型。这些类型比 RefCell 具有更低的内存开销,并且对代码生成只有很小的影响。
在多线程情况下,您可能希望使用MutexRwLock

是的,我将添加另一个条件来确保它不会发生(start == num)。感谢您的帮助! - marxin

10

使用 hashbrown::HashMap

如果您可以切换到使用hashbrown, 您可能可以使用类似于get_many_mut的方法:

use hashbrown::HashMap; // 0.12.1

fn main() {
    let mut map = HashMap::new();
    map.insert(1, true);
    map.insert(2, false);

    dbg!(&map);

    if let Some([a, b]) = map.get_many_mut([&1, &2]) {
        std::mem::swap(a, b);
    }

    dbg!(&map);
}

由于哈希布朗是标准库哈希映射表的动力源,因此在 Rust 的夜间版本中也可以使用 HashMap::get_many_mut

不安全代码

如果您可以保证两个索引不同,那么可以使用不安全代码来避免内部可变性:

use std::collections::HashMap;

fn get_mut_pair<'a, K, V>(conns: &'a mut HashMap<K, V>, a: &K, b: &K) -> (&'a mut V, &'a mut V)
where
    K: Eq + std::hash::Hash,
{
    unsafe {
        let a = conns.get_mut(a).unwrap() as *mut _;
        let b = conns.get_mut(b).unwrap() as *mut _;
        assert_ne!(a, b, "The two keys must not resolve to the same value");
        (&mut *a, &mut *b)
    }
}

fn main() {
    let mut map = HashMap::new();
    map.insert(1, true);
    map.insert(2, false);

    dbg!(&map);

    let (a, b) = get_mut_pair(&mut map, &1, &2);
    std::mem::swap(a, b);

    dbg!(&map);
}

类似的代码可以在诸如multi_mut之类的库中找到。

这段代码试图尽可能谨慎。在将它们转换回可变引用之前,断言强制执行两个值是不同的指针,并且我们明确地向返回的变量添加生命周期。

在盲目使用此解决方案之前,您应该了解不安全代码的细微差别。值得注意的是,之前版本的答案是不正确的。感谢 @oberien 找到原始实现中的不安全性并提出修复建议。此 playground演示了如何使用纯安全的 Rust 代码会导致旧代码导致内存不安全。

此解决方案的增强版本可以接受键数组并返回值数组:

fn get_mut_pair<'a, K, V, const N: usize>(conns: &'a mut HashMap<K, V>, mut ks: [&K; N]) -> [&'a mut V; N]

然而,确保所有传入的键都是唯一的变得更加困难。


请注意,此函数并没有尝试解决原始问题,原始问题比验证两个索引是否不相交要复杂得多。原始问题需要:

  • 跟踪三个 不相交的借用,其中两个是可变的,一个是不可变的。
  • 跟踪递归调用
    • 不能以使哈希映射重新分配大小的任何方式修改哈希映射,否则会使先前级别的任何现有引用无效。
    • 不能与以前级别中的任何引用别名。

使用像 RefCell 这样的东西是确保您不触发内存不安全的简单得多的方法。


实际上,你只需要将最后一行放在 unsafe 块中,其余部分可以移动到它之前。 - Peter Hall
另外,我不确定这完全解决了 OP 的问题(即使它回答了标题问题),因为他的示例代码需要再次可变地借用 HashMap,如果他只是像这样更新他的代码来使用你的函数:let (mut orig_conns, mut pipes) = get_mut_pair(conns, &start, &num); 就无法工作。 - Peter Hall
@PeterHall 不是故意的,但我忘了直接说明。谢谢你提醒我。 - Shepmaster
@Shepmaster 是的,请注意我在 OP 上也添加了一个链接到 multi_mut crate,其中包括用于变异 2/3/x 个对 HashMap 的引用的方法。 - ANimator120
1
get_many_mut() 方法现在已经被夜版支持:https://github.com/rust-lang/rust/issues/97601 - Sven Marnach
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接