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

ovfsdjhp  于 2022-12-19  发布在  其他
关注(0)|答案(2)|浏览(171)

下面的代码:

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_connspipes)。
Rust不允许我同时拥有可变变量和不可变变量,我有点困惑,因为这将是完全不同的对象,但我猜如果&start == &num,那么我将拥有对同一对象的两个不同引用(一个可变,一个不可变)。
这是可以的,但是我怎么才能做到呢?我想迭代一个HashSet,读取并修改另一个。让我们假设它们不是相同的HashSet

6ojccjat

6ojccjat1#

如果可以更改数据类型和函数签名,则可以使用RefCell创建interior mutability

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,则线程将死机,因为这是对同一HashSet同时进行可变和不可变访问的尝试。

RefCell的安全替代方案

根据你对数据和代码的需求,你也可以使用Cell或者atomics类型,这些类型比RefCell的内存开销要小,对代码生成的影响也很小。
在多线程的情况下,您可能希望使用MutexRwLock

xpcnnkqh

xpcnnkqh2#

使用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);
}

由于hashbrown是标准库hashmap的动力,因此在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这样的库中找到。
这段代码非常谨慎,Assert强制两个值是不同的指针,然后再将它们转换回可变引用,并且我们显式地为返回的变量添加了生存期。
在盲目使用这个解决方案之前,你应该了解不安全代码的细微差别。值得注意的是,previous versions of this answer是不正确的。感谢@oberien发现了这个问题的原始实现中的不合理之处,并提出了一个修复方案。This 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]

然而,确保所有传入密钥都是唯一的变得更加困难。
注意,这个函数并不试图解决原始问题,原始问题比验证 * 两个 * 索引不相交要复杂得多。原始问题要求:

  • 跟踪 * 三个 * 不相交的借入,其中两个是可变的,一个是不可变的。
  • 跟踪递归调用
  • 不能以任何可能导致调整大小的方式修改HashMap,这将使上一级别的任何现有引用无效。
  • 不能为上一级别的任何引用设置别名。

使用RefCell之类的东西是一种 * 简单得多 * 的方法,可以确保您不会触发内存不安全。

相关问题