如何在 Rust 中懒惰地创建其构造使用 self 的映射条目

How to lazily create map entry whose construction uses self in Rust

我正在尝试在 Rust 中实现惰性构造/记忆化评估/缓存惯用语。

有一个外部类型,它有一堆数据和一个访问器方法。访问器方法需要 return 一个缓存的计算(如果它有一个)或者计算它并将 return 值存储在映射中供以后使用。缓存值不需要引用外部值,所以不存在循环引用问题;但它确实需要访问外部值的数据才能构建自身。

这是一个没有通过 Rust 借用检查器的完整示例:

use std::collections::HashMap;

pub struct ContainedThing {
    count: usize,
}

impl ContainedThing {
    fn create(thing: &Thing) -> ContainedThing {
        // create uses an arbitrary number of attributes from Thing
        // it doesn't keep any references after returning though
        let count = thing.map.len();
        ContainedThing { count: count }
    }
}

struct Thing {
    map: HashMap<i32, ContainedThing>,
}

impl Thing {
    pub fn get(&mut self, key: i32) -> &ContainedThing {
        self.map
            .entry(key)
            .or_insert_with(|| ContainedThing::create(&self))
    }
}

fn main() {}

具体错误为:

error[E0502]: cannot borrow `self` as immutable because `self.map` is also borrowed as mutable
  --> src/main.rs:24:29
   |
22 |         self.map
   |         -------- mutable borrow occurs here
23 |             .entry(key)
24 |             .or_insert_with(|| ContainedThing::create(&self))
   |                             ^^                         ---- borrow occurs due to use of `self` in closure
   |                             |
   |                             immutable borrow occurs here
25 |     }
   |     - mutable borrow ends here

我真的很难找到实现这个习惯用法的好方法。我尝试模式匹配 get() 的 return 值而不是 entry() API,但仍然与借用检查器发生冲突:match 表达式也结束了借用 self.

我可以这样重写get

pub fn get(&mut self, key: i32) -> &ContainedThing {
    if !self.map.contains_key(&key) {
        let thing = ContainedThing::create(&self);
        self.map.insert(key, thing);
    }
    self.map.get(&key).unwrap()
}

但这很丑陋(看看那个 unwrap)并且似乎需要比应有的更多的查找和复制。理想情况下,我想要

  1. 只需支付一次查找哈希条目的费用。 entry(),做对了,应该在找不到时跟踪插入位置。
  2. 减少新构造值的副本数。这可能是不可行的,理想情况下我有一个就地结构。
  3. 避免使用unwrap;没有无意义的模式匹配,即。

我的笨拙代码是可以实现的最好的吗?

答案是具体取决于您需要在 or_insert_with 闭包中访问哪个状态。问题是 or_insert_with 绝对不能访问地图本身,因为条目 api 需要对地图进行可变借用。

如果您ContainedThing::create只需要地图的大小,那么您只需要提前计算地图大小即可。

impl Thing {
    pub fn get(&mut self, key: i32) -> &ContainedThing {
        let map_size = self.map.len();
        self.map.entry(&key).or_insert_with(|| {
            // The call to entry takes a mutable reference to the map,
            // so you cannot borrow map again in here
            ContainedThing::create(map_size)
        })
    }
}

我认为这个问题的精神更多地是关于一般策略,所以让我们假设 Thing 中还有一些其他状态也需要创建 ContainedThing

struct Thing {
    map: HashMap<i32, ContainedThing>,
    some_other_stuff: AnotherType, //assume that this other state is also required in order to create ContainedThing
}

impl Thing {
    pub fn get(&mut self, key: i32) -> &ContainedThing {
        //this is the borrow of self
        let Thing {
            ref mut map,
            ref mut some_other_stuff,
        } = *self;

        let map_size = map.len();
        map.entry(key).or_insert_with(|| {
            // map.entry now only borrows map instead of self
            // Here you're free to borrow any other members of Thing apart from map
            ContainedThing::create(map_size, some_other_stuff)
        })
    }
}

这是否真的比手动检查 if self.map.contains_key(&key) 的其他解决方案更干净尚有待商榷。不过,我倾向于采用解构策略,以允许借用 self 的特定成员而不是整个结构。

所以,我想将 Thing 作为参数传递给 ContainedThing::create 的主要动机是使用 Thing 的 API 来帮助构建。然而,事实证明我会希望它被可变地借用,因为我在这里需要递归记忆化构造,这使得它成为一个循环问题。

所以,看来我的分离检查 + 插入逻辑会保留下来。