如何在不依赖复制特征的情况下在Rust中构建Cacher?

时间:2018-11-16 04:18:47

标签: hashmap rust

我正在尝试实现Rust书Chapter 13中提到的Cacher并遇到麻烦。

我的Cacher代码如下:

use std::collections::HashMap;
use std::hash::Hash;

pub struct Cacher<T, K, V>
where
    T: Fn(K) -> V,
{
    calculation: T,
    values: HashMap<K, V>,
}

impl<T, K: Eq + Hash, V> Cacher<T, K, V>
where
    T: Fn(K) -> V,
{
    pub fn new(calculation: T) -> Cacher<T, K, V> {
        Cacher {
            calculation,
            values: HashMap::new(),
        }
    }

    pub fn value(&mut self, k: K) -> &V {
        let result = self.values.get(&k);
        match result {
            Some(v) => {
                return v;
            }
            None => {
                let v = (self.calculation)(k);
                self.values.insert(k, v);
                &v
            }
        }
    }
}

我对该库的测试用例如下:

mod cacher;

#[cfg(test)]
mod tests {
    use cacher::Cacher;

    #[test]
    fn repeated_runs_same() {
        let mut cacher = Cacher::new(|x| x);
        let run1 = cacher.value(5);
        let run2 = cacher.value(7);

        assert_ne!(run1, run2);
    }
}

运行测试用例时遇到以下问题:

  1. error[E0499]: cannot borrow cacher as mutable more than once at a time 每次我生成run1,run2值时,它都会尝试借用cacher作为可变借项。我根本不明白为什么要借用-我认为cacher.value()应该返回对存储在cacher中的项目的引用,而不是借用。
  2. error[E0597]: v does not live long enough指向v我在value()为None的情况下返回。如何正确地将v移到HashMap并赋予其与HashMap相同的生存期?显然,当它返回时,生存期将到期,但是我只想返回对其的引用,以用作value()的返回。
  3. value()中的
  4. error[E0502]: cannot borrow self.values as mutable because it is also borrowed as immutableself.values.get(&k)是不可变的借款,self.values.insert(k,v)是可变的借款-尽管我认为.get()是不可变的借款,而.insert()是所有权转让。

和其他一些与移动有关的错误,我应该能够分别处理。这些是更根本的错误,表明我对Rust的所有权概念有误解,但重新阅读本书的内容并不清楚我错过了什么。

2 个答案:

答案 0 :(得分:2)

我认为这里有很多问题需要考虑:

首先,对于函数value(&mut self, k: K) -> &V的定义;编译器将为您插入生存期,使其变为value(&'a mut self, k: K) -> &'a V。这意味着self的生存期不会因函数而缩短,因为存在具有相同生存期的引用离开函数,并且生存期与作用域一样长。由于它是可变参考,因此您不能再次借用它。因此出现错误error[E0499]: cannot borrow cacher as mutable more than once at a time

第二,调用calculation函数,该函数返回函数value()的某些内部范围内的值,然后返回对其的引用,这是不可能的。您期望参考的寿命比参考对象的寿命长。因此,错误error[E0597]: v does not live long enough

第三个错误涉及其中。您会看到,第一条语句中提到的let result = self.values.get(&k);导致k保持不变,直到函数结束。返回的result将在您的函数value()中有效,这意味着您不能在同一范围内借用(可变),从而导致错误 error[E0502]: cannot borrow self.values as mutable because it is also borrowed as immutable in value() self.values.get(&k)

您的K必须是Clone,原因是k将被移到函数calculation中,从而使其在insert中不可用。

因此,将K作为CloneCacher的实现将是:

impl<T, K: Eq + Hash + Clone, V> Cacher<T, K, V>
where
    T: Fn(K) -> V,
{
    pub fn new(calculation: T) -> Cacher<T, K, V> {
        Cacher {
            calculation,
            values: hash_map::HashMap::new(),
        }
    }

    pub fn value(&mut self, k: K) -> &V {
        if self.values.contains_key(&k) {
            return &self.values[&k];
        }

        self.values.insert(k.clone(), (self.calculation)(k.clone()));
        self.values.get(&k).unwrap()
    }
}

此处的生存期基于分支控制流。 if self.values.contains_key ...块总是返回,因此if块之后的代码仅在if self.values.contains_key ...false时才能执行。为if条件创建的微小作用域将仅存在于条件检查中,即为if self.values.contains_key(...获取(并返回)的引用将被该微小作用域所取代。

有关更多信息,请参阅NLL RFC

正如@jmb在回答中所提到的那样,为了使测试正常进行,V必须是Cloneimpl <... V:Clone> Cacher<T, K, V>)才能按价值返回或使用{ {1}}以避免克隆费用。

例如

Rc

答案 1 :(得分:2)

  1. 返回对值的引用与借用该值相同。由于该值归缓存器所有,因此它也隐式借用了缓存器。这是有道理的:如果您引用了缓存器中的某个值,然后销毁了该缓存器,您的引用又会怎样?还请注意,如果您修改了缓存器(例如,通过插入新元素),则可能会重新分配存储空间,这会使对存储在其中的值的任何引用无效。

    您需要您的值至少为Clone,以便Cacher::value可以按值而不是按引用返回。如果您的值过于昂贵而无法克隆,并且所有调用方都获得相同的实例是可以的,则可以使用Rc

  2. 获取存储在HashMap中的实例(而不是您为其分配的临时实例)的幼稚方式是在将值插入映射后调用self.values.get (k).unwrap()。为了避免计算值在地图中位置的两倍的开销,可以使用Entry界面:

    pub fn value(&mut self, k: K) -> Rc<V> {
        self.values.entry (&k).or_insert_with (|| Rc::new (self.calculation (k)))
    }
    
  3. 我相信我对第2点的回答也解决了这一点。