Reputation: 2060
I am using HashMap
but I'm stumbling over how to "release" a mutable borrow of the HashMap
and cannot find a good explanation of how to do this.
This is just an example and that the goal is not to "solve the problem" but rather to understand how to accomplish this and/or why it should not be done this way.
The example consists of a HashMap
storing some simple Record
s:
type Map = HashMap<String, Record>;
pub struct Record {
pub count: u32,
pub name: String,
}
impl Record {
fn new<S: Into<String>>(name: S) -> Record {
Record { name: name.into(), count: 0 }
}
pub fn add<'a>(&'a mut self, inc: u32) -> &'a mut Record {
self.count += inc;
self
}
}
The add
function is there to have a mutable function on the record but is not the real culprit here.
We now want to implement a function that returns a reference to a Record
in the HashMap
so that we can modify it in-place. In addition to this, we want to be able to have control over the returned reference so that we can do some side effects (for this example it is sufficient that we assume that we want to print out what is happening, but it could be some other action to handle statistics and/or accessing some other storage or do lazy evaluation). To handle this, we introduce a Handle
struct that keeps a reference to the Record
as well as a reference to the HashMap
that the record is coming from.
pub struct Handle<'a> {
map: &'a Map,
record: &'a Record,
}
impl<'a> Handle<'a> {
fn new(record: &'a Record, map: &'a Map) -> Handle<'a> {
println!("Retrieving record");
Handle { record: record, map: map }
}
fn mut_record(&mut self) -> &mut Record {
println!("Modifying record");
self.record
}
}
Let's assume that we need both references for some reason and note that we are fine with retaining an immutable borrow to the HashMap
while the handle is in existence, so no modifications of the HashMap
should happen.
The Handle
is just temporary and we expect that it can be used roughly like this:
let mut map = HashMap::new();
let foo = get_or_insert(&mut map, "foo");
foo.mut_record().do_something(|record| record.add(3))
The first implementation of get_or_insert
is this:
pub fn get_or_insert<'a, S>(map: &'a mut Map, name: S) -> Handle<'a>
where S: Into<String>
{
let key = name.into();
let record = map.entry(key.clone()).or_insert(Record::new(key));
Handle::new(record, map)
}
This gives the following error:
error[E0502]: cannot borrow `*map` as immutable because it is also borrowed as mutable
--> hashmap.rs:65:29
|
64 | let record = map.entry(key.clone()).or_insert(Record::new(key));
| --- mutable borrow occurs here
65 | Handle::new(record, map)
| ^^^ immutable borrow occurs here
66 | }
| - mutable borrow ends here
There are two references to the HashMap
with the first one being a mutable borrow. We need to "release" the first mutable borrow of the map before we can grab an immutable borrow. I tried to write the code in this manner and added a scope around the first mutable borrow expecting it to be "released" when the scope ends:
pub fn get_or_insert<'a, S>(map: &'a mut Map, name: S) -> Handle<'a>
where S: Into<String>
{
let key = name.into();
let record = {
map.entry(key.clone()).or_insert(Record::new(key))
};
Handle::new(record, map)
}
But the error remains.
It is very strange that the mutable borrow remain even after the scope has finished. According to References and Borrowing the borrow should end at the end of the scope, and according to Scope and shadowing scopes are controlled by blocks, which are collections of statements enclosed in braces, so superficially, it appears that the later function definition should end the scope with the mutable borrow of the reference to the map.
How do you implement a Handle
like this in reasonable way so that the lifetime of Handle
does not exceed that of the HashMap
and capture this at compile time? I'm looking at a good way to create a Handle
that
Abstract away the access to the underlying storage by using the temporary Handle
as the abstraction provided by the implementation.
Capture misuses at compile time rather than runtime, which disqualifies RefCell
and Rc
.
Perform a single lookup in the underlying structure.
I looked at RefCell
but that moves the checks from compile time to runtime, and it would be beneficial to be able to catch misuses of Handle
at compile time.
The question in Rust: Borrowing issues with attempted caching is similar to this one but the answer is using UnsafeCell
, which work around the checks rather than solve them.
To me the problem seems to be that there need to be a way to transform a mutable reference to a immutable reference and release the mutable borrow (with the restriction that it should be permitted by the code), but still not sure if I have misunderstood something.
Update: There were originally three bullets for this question in an attempt to make it more structured, but it's been rewritten to just pose a single question to make it clear what the goal is.
Upvotes: 1
Views: 464
Reputation: 65692
On this line:
let record = map.entry(key.clone()).or_insert(Record::new(key));
record
is of type &'a mut Record
, because or_insert
returns a mutable reference to the value stored in the HashMap
. This keeps the borrow on map
active; that's why you get the error.
One solution is to lookup the value using get
after the insertion in order to obtain an immutable borrow.
pub fn get_or_insert<'a, S>(map: &'a mut Map, name: S) -> Handle<'a>
where S: Into<String>
{
let key = name.into();
map.entry(key.clone()).or_insert(Record::new(key.clone()));
let record = map.get(&key).unwrap();
Handle::new(record, map)
}
Note that this still doesn't let you implement Handle::mut_record
with the signature you gave; Handle
only has immutable references to the map and to the record, and you can't obtain a mutable reference to a record with those.
Upvotes: 1