16

I have a RefCell<HashMap> and want to borrow the table, find a key, and return a reference to the result:

use std::cell::RefCell;
use std::collections::HashMap;

struct Frame {
    map: RefCell<HashMap<String, String>>,
}

impl Frame {
    fn new() -> Frame {
        Frame {
            map: RefCell::new(HashMap::new()),
        }
    }

    fn lookup<'a>(&'a self, k: &String) -> Option<&'a String> {
        self.map.borrow().get(k)
    }
}

fn main() {
    let f = Frame::new();
    println!("{}", f.lookup(&"hello".to_string()).expect("blargh!"));
}

(playground)

If I remove the RefCell then everything works okay:

struct Frame {
    map: HashMap<String, String>,
}

impl Frame {
    fn lookup<'a>(&'a self, k: &String) -> Option<&'a String> {
        self.map.get(k)
    }
}

What is the correct way to write the lookup function without copying the string in the hashtable?

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
Marc Miller
  • 161
  • 1
  • 6

1 Answers1

19

When you borrow from a RefCell, the reference you get has a shorter lifetime than the RefCell's. That's because the reference's lifetime is restricted by the guard returned by borrow(). That guard ensures that nobody else can take a mutable reference to the value until the guard is dropped.

However, you are trying to return a value without keeping a guard alive. If Frame had a method that took a &self argument but tried to mutate the map (which is possible with RefCell — if you don't need to do that, then ditch the RefCell and write &mut self on the methods that mutate the map), you could accidentally destroy a String that somebody else has a reference to. That is exactly the kind of errors that the borrow checker was designed to report!

If the map values are effectively immutable (i.e. your type will not allow mutating the map's values), you could also wrap them in an Rc in your map. You could therefore return a clone of the Rc<String> (this only clones the reference-counted pointer, not the underlying string), which would let you release the borrow on the map before returning from the function.

struct Frame {
    map: RefCell<HashMap<String, Rc<String>>>
}

impl Frame {
    fn lookup(&self, k: &String) -> Option<Rc<String>> {
        self.map.borrow().get(k).map(|x| x.clone())
    }
}
Peter Hall
  • 53,120
  • 14
  • 139
  • 204
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
  • Hey thanks. This is a great explanation. I'm still learning, so I've been wrestling with the borrow checker, but of course this makes sense. I suppose I don't quite understand why the RefCell makes the difference -- why doesn't the non-RefCell version also also cause problem for the same reason? – Marc Miller May 17 '15 at 03:56
  • @MarcMiller: I suppose that by now you have realized that Rust is all about ownership and borrowing. Normally, those get checked at compile-time, however there are constructs based on `unsafe` code which lie to the compiler and instead verify the correctness at run-time. `RefCell` is such a construct, and therefore introducing it changes the rules slightly; you can think of it as a Read-Write Mutex for single-threaded code. – Matthieu M. May 17 '15 at 12:46
  • @matthieu-m: Thank you for chiming in! Could I trouble you to clarify? Are you saying that the RefCell changes this to a sort-of writer lock instead of just a reader lock, even though I'm not doing borrow_mut? I just really don't want to have to clone my return values here since my objects can be quite large, so I'm looking at possibly doing something like ref-counting: RefCell> -- but that makes me feel like I'm doing something way wrong. Thanks. – Marc Miller May 17 '15 at 19:01
  • @MarcMiller: If you're not using `borrow_mut()` at all, then there's absolutely no point in using `RefCell`. Just store the `HashMap` directly in your `Frame`. If you want to avoid copies/clones, then return a reference, that's their purpose! – Francis Gagné May 18 '15 at 06:13
  • 1
    @MarcMiller: `RefCell` is in essence a reader-writer lock, though not thread-safe; when you `.borrow_mut()` it checks that nobody else already has a borrow (writer requires exclusive access) and when you `.borrow()` it checks that no writer is active (reader get shared access). In order for the `RefCell` to guarantee writer exclusivity though, it means that what you borrow through `.borrow()` cannot live longer than the guard (`Ref<'a, T>`), which is what you are experiencing here (since the guard does not live longer than your function). Returning a `Rc` has advised works around this. – Matthieu M. May 18 '15 at 06:16