简体   繁体   中英

How do I build a Cacher in Rust without relying on the Copy trait?

I am trying to implement a Cacher as mentioned in Chapter 13 of the Rust book and running into trouble.

My Cacher code looks like:

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
            }
        }
    }
}

and my test case for this lib looks like:

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);
    }
}

I ran into the following problems when running my test case:

  1. error[E0499]: cannot borrow cacher as mutable more than once at a time Each time I make a run1, run2 value it is trying to borrow cacher as a mutable borrow. I don't understand why it is borrowing at all - I thought cacher.value() should be returning a reference to the item that is stored in the cacher which is not a borrow.
  2. error[E0597]: v does not live long enough pointing to the v I return in the None case of value(). How do I properly move the v into the HashMap and give it the same lifetime as the HashMap ? Clearly the lifetime is expiring as it returns, but I want to just return a reference to it to use as the return from value().
  3. error[E0502]: cannot borrow self.values as mutable because it is also borrowed as immutable in value(). self.values.get(&k) is an immutable borrow and self.values.insert(k,v) is a mutable borrow - though I thought .get() was an immutable borrow and .insert() was a transfer of ownership.

and a few other errors related to moving which I should be able to handle separately. These are much more fundamental errors that indicate I have misunderstood the idea of ownership in Rust, yet rereading the segment of the book doesn't make clear to me what I missed.

I think there a quite a few issues to look into here:

First, for the definition of the function value(&mut self, k: K) -> &V ; the compiler will insert the lifetimes for you so that it becomes value(&'a mut self, k: K) -> &'a V . This means, the lifetime of the self cannot shrink for the sake of the function, because there is reference coming out of the function with the same lifetime, and will live for as long as the scope. Since it is a mutable reference, you cannot borrow it again. Hence the error error[E0499]: cannot borrow cacher as mutable more than once at a time .

Second, you call the calculation function that returns the value within some inner scope of the function value() and then you return the reference to it, which is not possible. You expect the reference to live longer than the the referent. Hence the error error[E0597]: v does not live long enough

The third error is a bit involved. You see, let result = self.values.get(&k); as mentioned in the first statement, causes k to be held immutably till the end of the function. result returned will live for as long your function value() , which means you cannot take a borrow(mutable) in the same scope, giving the error error[E0502]: cannot borrow self.values as mutable because it is also borrowed as immutable in value() self.values.get(&k)

Your K needs to be a Clone , reason being k will be moved into the function calculation , rendering it unusable during insert .

So with K as a Clone , the Cacher implementation will be:

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()
    }
}

This lifetimes here are based on the branching control flow. The if self.values.contains_key ... block always returns, hence the code after if block can only be executed when if self.values.contains_key ... is false . The tiny scope created for if condition, will only live within the condition check, ie reference taken (and returned) for if self.values.contains_key(... will go away with this tiny scope.

For more please refer NLL RFC

As mentioned by @jmb in his answer, for your test to work, V will need to be a Clone ( impl <... V:Clone> Cacher<T, K, V> ) to return by value or use shared ownership like Rc to avoid the cloning cost.

eg.

fn value(&mut self, k: K) -> V  { ..
fn value(&mut self, k: K) -> Rc<V> { ..
  1. Returning a reference to a value is the same thing as borrowing that value. Since that value is owned by the cacher, it implicitly borrows the cacher too. This makes sense: if you take a reference to a value inside the cacher then destroy the cacher, what happens to your reference? Note also that if you modify the cacher (eg by inserting a new element), this could reallocate the storage, which would invalidate any references to values stored inside.

    You need your values to be at least Clone so that Cacher::value can return by value instead of by reference. You can use Rc if your values are too expensive to clone and you are ok with all callers getting the same instance.

  2. The naive way to get the instance that was stored in the HashMap as opposed to the temporary you allocated to build it would be to call self.values.get (k).unwrap() after inserting the value in the map. In order to avoid the cost of computing twice the location of the value in the map, you can use the Entry interface:

     pub fn value(&mut self, k: K) -> Rc<V> { self.values.entry (&k).or_insert_with (|| Rc::new (self.calculation (k))) } 
  3. I believe my answer to point 2 also solves this point.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM