Rust:在完全不可变地借用整个 HashMap 的同时修改其中的值

4

我正在尝试通过在我的项目中使用Rust来学习它。

然而,在某些代码中,我遇到了借用检查器的问题,其形式与以下代码非常相似:

use std::collections::HashMap;
use std::pin::Pin;
use std::vec::Vec;

struct MyStruct<'a> {
    value: i32,
    substructs: Option<Vec<Pin<&'a MyStruct<'a>>>>,
}

struct Toplevel<'a> {
    my_structs: HashMap<String, Pin<Box<MyStruct<'a>>>>,
}

fn main() {
    let mut toplevel = Toplevel {
        my_structs: HashMap::new(),
    };

    // First pass: add the elements to the HashMap
    toplevel.my_structs.insert(
        "abc".into(),
        Pin::new(Box::new(MyStruct {
            value: 0,
            substructs: None,
        })),
    );
    toplevel.my_structs.insert(
        "def".into(),
        Pin::new(Box::new(MyStruct {
            value: 5,
            substructs: None,
        })),
    );
    toplevel.my_structs.insert(
        "ghi".into(),
        Pin::new(Box::new(MyStruct {
            value: -7,
            substructs: None,
        })),
    );

    // Second pass: for each MyStruct, add substructs
    let subs = vec![
        toplevel.my_structs.get("abc").unwrap().as_ref(),
        toplevel.my_structs.get("def").unwrap().as_ref(),
        toplevel.my_structs.get("ghi").unwrap().as_ref(),
    ];
    toplevel.my_structs.get_mut("abc").unwrap().substructs = Some(subs);
}

编译时,我收到了以下消息:

error[E0502]: cannot borrow `toplevel.my_structs` as mutable because it is also borrowed as immutable
  --> src/main.rs:48:5
   |
44 |         toplevel.my_structs.get("abc").unwrap().as_ref(),
   |         ------------------- immutable borrow occurs here
...
48 |     toplevel.my_structs.get_mut("abc").unwrap().substructs = Some(subs);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^--------------------
   |     |
   |     mutable borrow occurs here
   |     immutable borrow later used here

我认为我理解了这种情况发生的原因:toplevel.my_structs.get_mut(...)toplevel.my_structs 借为可变状态。然而,在同一块中,toplevel.my_structs.get(...) 也借用了 toplevel.my_structs(这次是不可变状态)。
我也看到如果借用 &mut toplevel.my_structs 函数添加了一个新键,那么这确实会成为一个问题。
然而,这里在 &mut toplevel.my_structs 的借用中所做的所有工作都只是修改与特定键对应的值,这不应该改变内存布局(由于 Pin 的保证可以得到)。 对吗?
是否有一种方法可以将此信息传达给编译器,以便我可以编译此代码?这似乎与激励 hashmap::Entry API 类似,但我需要能够访问其他键,而不仅仅是要修改的一个键。
2个回答

2
你目前的问题是有可变借用和不可变借用之间的冲突,但这里有一个更深层次的问题。这个数据结构不能用于你所尝试做的事情:
struct MyStruct<'a> {
    value: i32,
    substructs: Option<Vec<Pin<&'a MyStruct<'a>>>>,
}

struct Toplevel<'a> {
    my_structs: HashMap<String, Pin<Box<MyStruct<'a>>>>,
}

任何时候,如果一个类型有一个生命周期参数,那么这个生命周期必须比该类型的值存在时间更长(或者与该类型的值存在时间相同)。包含引用&'a MyStruct的容器Toplevel<'a>必须引用在Toplevel之前创建的MyStructs,除非您使用像区域分配器这样的特殊工具。
(可以直接构建引用树,但必须从叶子开始构建,不能使用递归算法;这通常对于动态输入数据来说是不切实际的。)
一般来说,引用并不适合创建数据结构;它们更适合于“暂时”借用数据结构的部分。
在您的情况下,如果您想拥有所有MyStructs的集合,并且还希望能够在它们创建后添加它们之间的连接,则需要共享所有权和内部可变性。
use std::collections::HashMap;
use std::cell::RefCell;
use std::rc::Rc;

struct MyStruct {
    value: i32,
    substructs: Option<Vec<Rc<RefCell<MyStruct>>>>,
}

struct Toplevel {
    my_structs: HashMap<String, Rc<RefCell<MyStruct>>>,
}

通过使用 Rc 进行共享所有权,Toplevel 和任意数量的 MyStruct 可以引用其他 MyStruct。通过使用 RefCell 进行内部可变性,即使在整个数据结构的其他元素中引用它时,MyStructsubstructs 字段也可以被修改。
有了这些定义,您可以编写所需的代码:
fn main() {
    let mut toplevel = Toplevel {
        my_structs: HashMap::new(),
    };

    // First pass: add the elements to the HashMap
    toplevel.my_structs.insert(
        "abc".into(),
        Rc::new(RefCell::new(MyStruct {
            value: 0,
            substructs: None,
        })),
    );
    toplevel.my_structs.insert(
        "def".into(),
        Rc::new(RefCell::new(MyStruct {
            value: 5,
            substructs: None,
        })),
    );
    toplevel.my_structs.insert(
        "ghi".into(),
        Rc::new(RefCell::new(MyStruct {
            value: -7,
            substructs: None,
        })),
    );

    // Second pass: for each MyStruct, add substructs
    let subs = vec![
        toplevel.my_structs["abc"].clone(),
        toplevel.my_structs["def"].clone(),
        toplevel.my_structs["ghi"].clone(),
    ];
    toplevel.my_structs["abc"].borrow_mut().substructs = Some(subs);
}

请注意,因为你让"abc"引用了自身,这将创建一个引用循环,当删除Toplevel时,它将不会被释放。要解决此问题,您可以impl Drop for Toplevel并显式删除所有substructs引用。


另一个选择是使用索引进行交叉引用,这可能更符合Rust的风格。这种方法有一些优点和缺点:

  • 增加了额外的哈希查找成本。
  • 删除了引用计数和内部可变性的成本。
  • 可能会出现“悬挂引用”的情况:从映射中删除键,从而使对其的引用无效。
use std::collections::HashMap;

struct MyStruct {
    value: i32,
    substructs: Option<Vec<String>>,
}

struct Toplevel {
    my_structs: HashMap<String, MyStruct>,
}

fn main() {
    let mut toplevel = Toplevel {
        my_structs: HashMap::new(),
    };

    // First pass: add the elements to the HashMap
    toplevel.my_structs.insert(
        "abc".into(),
        MyStruct {
            value: 0,
            substructs: None,
        },
    );
    toplevel.my_structs.insert(
        "def".into(),
        MyStruct {
            value: 5,
            substructs: None,
        },
    );
    toplevel.my_structs.insert(
        "ghi".into(),
        MyStruct {
            value: -7,
            substructs: None,
        },
    );

    // Second pass: for each MyStruct, add substructs
    toplevel.my_structs.get_mut("abc").unwrap().substructs =
        Some(vec!["abc".into(), "def".into(), "ghi".into()]);
}

哦,我现在明白了,谢谢!我之前确实考虑过“只使用索引”的方法,但是因为你提到的第一和第三点以及这意味着在使用数据结构时需要更多的代码(与仅使用引用相比),所以我拒绝了它。只是一个问题;使用Rc<RefCell<_>>真的是解决这个问题并仍然使用“引用”的唯一方法吗?对于这个问题,使用引用计数似乎有点不必要。是否有一些参考页面可以查看可能的替代方案? - dccsillag
另外,非常感谢您关于循环引用的警告。我肯定会错过它的。 - dccsillag
参考计数是标准库中唯一可用的安全选项,使得一个对象可以通过多个路径访问而不需要强制使用临时引用。正如您在问题中所指出的那样,您可以使用裸指针来避免参考计数,但这时您需要确保对象实际上没有被释放或移动。 - Kevin Reid

0
在您的代码中,您试图修改作为不可变的向量引用的值,这是不允许的。您可以使用可变引用替代向量并直接对它们进行修改,例如:
let subs = vec![
    toplevel.my_structs.get_mut("abc").unwrap(),
    toplevel.my_structs.get_mut("def").unwrap(),
    toplevel.my_structs.get_mut("ghi").unwrap(),
];
(*subs[0]).substructs = Some(subs.clone());

然而,存储结构体的克隆比存储引用更容易(尽管更昂贵):

let subs = vec![
    toplevel.my_structs.get("abc").unwrap().clone(),
    toplevel.my_structs.get("def").unwrap().clone(),
    toplevel.my_structs.get("ghi").unwrap().clone(),
];
(*toplevel.my_structs.get_mut("abc").unwrap()).substructs = Some(subs);

我真的吗?以我的看法,我的代码将用另一个Option<Vec<_>>交换整个值,而且由于元素类型是引用(不是所有者),所以这不应该使任何内存无效,并且编译器应该意识到这一点。 - dccsillag
另外,在第二次遍历中,我只能通过输入了解应在哈希映射表中查找的键,因此无法像您建议的那样将内容存储在数组/向量中。虽然克隆会使事情变得更容易,但我预计这将占用相当多的内存,因此我真的希望引用可以正常工作。 - dccsillag
当调用 get_mut 时,您正在使用对整个哈希映射的可变引用,而不仅仅是要更改的特定值。危险在于,您可能会更改向量中不可变引用的值(这正是您所做的)。当以不可变方式借用某些内容(如 hashmap.get 所做的那样)时,您需要能够依赖于它所引用的值永远不会改变。 - Spu7Nix
我认为你的意思是“在你的哈希映射中”。(而且它应该是一个HashMap,而不是一个vector,因为我想要快速访问给定的键)。但是,是的,我看到了,谢谢。你有什么建议吗(请原谅双关语:P),我如何只借用哈希映射的一个成员作为可变的(而不是将哈希映射也作为可变的借用)? - dccsillag
只是一个更新:事实证明指针游戏确实很好,因为可以通过指针和不安全块轻松解决这个问题。但我宁愿不这样做,以便我有一些保证,我没有搞砸事情(特别是在未来的重构之后)。 - dccsillag

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