How do I borrow a RefCell<HashMap>, find a key, and return a reference to the result? [duplicate]
Asked Answered
T

1

17

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?

Tinct answered 16/5, 2015 at 23:9 Comment(0)
L
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())
    }
}
Littman answered 17/5, 2015 at 0:29 Comment(5)
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?Tinct
@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.Martijn
@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<HashMap<String, Rc<String>> -- but that makes me feel like I'm doing something way wrong. Thanks.Tinct
@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!Loyalist
@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<String> has advised works around this.Martijn

© 2022 - 2024 — McMap. All rights reserved.