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

Bar*_*lly 5 hashmap rust borrow-checker

我正在尝试在 Rust 中实现延迟构造/记忆评估/缓存习语。

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

这是一个完整的例子,它没有通过 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() {}
Run Code Online (Sandbox Code Playgroud)

具体错误是:

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() {}
Run Code Online (Sandbox Code Playgroud)

我真的很难找到实现这个习语的好方法。我尝试模式匹配get()而不是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()
}
Run Code Online (Sandbox Code Playgroud)

但这非常难看(看看那个unwrap)并且似乎需要比应该更多的查找和副本。理想情况下,我希望

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

我笨拙的代码是最好的吗?

all*_*ons 4

答案是,这具体取决于您需要在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)
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

不过,我认为这个问题的精神更多的是关于一般策略,所以让我们假设其中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)
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

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