【问题标题】:Rust: Modify value in HashMap while immutably borrowing the whole HashMapRust:修改 HashMap 中的值,同时不可变地借用整个 HashMap
【发布时间】:2021-07-22 16:21:16
【问题描述】:

我正在尝试通过在我的项目中使用 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(尽管这次是不可变的)。

我还看到如果借用 &amp;mut toplevel.my_structs 的函数添加一个新密钥,这确实会成为一个问题。

但是,在&amp;mut toplevel.my_structs 借用中所做的只是修改与特定键对应的值,这不应该改变内存布局(这是有保证的,感谢Pin)。对吧?

有没有办法将它传达给编译器,以便我可以编译这段代码?这似乎有点类似于激发hashmap::Entry API 的原因,但我还需要能够访问其他密钥,而不仅仅是我想要修改的那个。

【问题讨论】:

    标签: rust borrow-checker


    【解决方案1】:

    您当前的问题是关于可变和不可变借用的冲突,但这里有一个更深层次的问题。此数据结构无法适用于您正在尝试做的事情:

    struct MyStruct<'a> {
        value: i32,
        substructs: Option<Vec<Pin<&'a MyStruct<'a>>>>,
    }
    
    struct Toplevel<'a> {
        my_structs: HashMap<String, Pin<Box<MyStruct<'a>>>>,
    }
    

    任何时候一个类型有一个生命周期参数,这个生命周期必然比该类型的值长(或完全一样长)。包含引用 &amp;'a MyStruct 的容器 Toplevel&lt;'a&gt; 必须引用 MyStructs Toplevel 之前创建 - 除非您使用像 an arena allocator 这样的特殊工具。

    (可以直接构建引用树,但必须先构建叶子,而不是使用递归算法;这对于动态输入数据通常是不切实际的。)

    一般来说,引用并不真正适合创建数据结构;而是它们暂时“借用”数据结构的一部分。

    在您的情况下,如果您想要拥有所有 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 和任意数量的MyStructs 引用其他MyStructs。通过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 引用。


    另一种选择,可以说更“生锈”是只使用索引进行交叉引用。这有几个优点和缺点:

    • 增加额外哈希查找的成本。
    • 消除了引用计数和内部可变性的成本。
    • 可以有“悬空引用”:可以从映射中删除一个键,使对它的引用无效。
    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&lt;RefCell&lt;_&gt;&gt; 真的是解决这个问题的唯一方法,同时仍然使用“引用”吗?为此使用引用计数似乎有点不必要。是否有一些参考页面可以让我查看可能的替代方案?
    • 另外,非常感谢您对循环引用的警告。我肯定会错过的。
    • @dccsillag 引用计数是标准库中唯一可用的安全选项,可以通过多个路径访问对象,而无需强制临时-参考性。正如您在问题中指出的那样,您可以使用原始指针来避免引用计数,但是您需要确保对象实际上没有被释放或移动。
    【解决方案2】:

    在您的代码中,您试图将向量中引用的值修改为不可变的,这是不允许的。您可以将可变引用存储在向量中,然后直接对其进行变异,如下所示:

    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&lt;Vec&lt;_&gt;&gt; 与另一个Option&lt;Vec&lt;_&gt;&gt; 交换,并且由于元素类型是引用(不属于自己),这不应该使任何内存无效,编译器应该知道的那个。
    • 另外,我只知道如何通过输入填充第二遍中的值,这为我提供了我应该寻找的哈希图中的键。所以我不能像你建议的那样将东西存储在数组/vec中。虽然克隆确实会让事情变得更容易,但我预计这会占用大量内存,所以我真的希望引用工作。
    • 调用get_mut 时,您使用的是对整个哈希图的可变引用,而不仅仅是您想要更改的特定值。危险在于您可以更改向量中不可变引用的值(您正在这样做)。当借用不可变的东西时(如 hashmap.get 所做的那样),您需要能够依赖它所引用的值永远不会改变。
    • 我认为您的意思是“在您的哈希图中”。 (它有点需要是一个 HashMap,而不是一个向量,因为我希望在给定密钥的情况下快速访问)。但是,是的,我明白了,谢谢。关于如何仅借用 hashmap 的一个成员作为可变成员(而不借用 hashmap 作为可变成员)的任何指针(请原谅双关语:P)?
    • 只是一个更新:事实证明指针双关语实际上非常好,因为这可以通过指针和不安全块轻松解决​​。但我宁愿不这样做,这样我就可以保证我不会把事情搞砸(尤其是在未来的重构之后)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-26
    • 1970-01-01
    • 2019-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多