How to avoid clone() / make this more idiomatic

I'm writing a server implementation for a multiplayer game (without the networking part atm). The server owns two hashmaps: connected and disconnected, of type HashMap<CharacterId, CharacterState>.

It has a connect method of type (&mut self, character_id: &CharacterId) -> Result<...> that looks up the id in the disconnected map, and moves it into the connected map. So far, so good.

The issue is that it also immutably borrows the CharacterState from one of the maps, and has to call another server method, join_scene, that mutably borrows self and immutably borrows that character state.

That doesn't work:

let state = self.disconnected.get(character_id).unwrap(); // immutable borrow occurs here
self.join_scene(state); // cannot borrow *self as mutable because it is also borrowed as immutable

I understand the issue: if join_scene mutably borrows self, then it could change the CharacterState immutably borrowed by state, which breaks Rust's rules.

I'm wondering what's the idiomatic way to handle situations like this:

  • clone() a part of the state: I will probably only ever need just two strings from CharacterState, realistically it won't be a performance issue
  • using Rc or the like: seems like an overkill, but I'm just learning Rust so I might be wrong and this is an acceptable solution
  • a different approach to the design: any suggestions about how to avoid the issue altogether?
  • some other trick

Since the borrow checker is able to reason about separate borrowing of different fields, you can in general just move around the fields of self that join_scene needs to borrow, ie. borrow them at the place of the call, and pass them in as separate arguments instead of borrowing the entirety of self at once.

This is not especially pretty, but if – hopefully – your function doesn't do a lot of unrelated stuff at once, you shouldn't need to create more than one or two more parameters.

This might also well be an indication that your state type contains unrelated components and you should break it up into smaller types, each with a different, disjoint subset of the fields of the original, big state type.

But to illustrate the first approach/workaround with code:

fn join_scene(state: &CharacterId, other_field: &mut OtherType) {
    …
}

let state = &self.disconnected[character_id];
join_scene(state, &mut self.other_field);

(As an aside, you can also replace .get().unwrap() with [indexing] syntax.)

2 Likes

Ah thanks that will probably suffice!

Which state type are you referring to? The whole server struct, or CharacterState? Either way I think it might be too early to say, I'm just prototyping as I learn Rust, but I don't think this is the case yet.

Whichever type the compiler is complaining about the simultaneous borrow of, because that's why you can solve it by breaking it up into sets of disjoint fields. I.e. that would be the type of self in your example.

1 Like

Ah okay, I'll definitely give this some thought, it just seems a bit hard at the moment as the Server struct (the type of self) is a place that sort of wraps everything together, at least in my head. Currently the implementation is so early that I don't really see what to remove from it. Also, whichever fields I remove and wrap in their own struct--would almost certainly end up as a member of the Server struct in that way, that's at least how I see it atm. Maybe there is a better approach.

You may appreciate this blog post, which talks about these patterns: https://smallcultfollowing.com/babysteps/blog/2018/11/01/after-nll-interprocedural-conflicts/ There are a few related posts on that blog, all of which include "After NLL" in the title.

1 Like

Actually if you are anyways going to move CharacterState from disconnected to connected why not just take it from the hashmap. It will move ownership to state variable, hence self should be unbound too and it won't make copy or clone, just clean hashmap's cell and transfer ownership to value:

let state = self.disconnected.remove(character_id).unwrap();
self.join_scene(state); 

Otherwise you also can move this logic to join_scene method.

 fn join_scene(&mut self, character_id: &CharacterId) ... {
    let state = self.disconnected.get(character_id).unwrap();
    ....
 }
3 Likes

The missing feature that would be required for this code to be accepted has been talked about a few times and even has an old RFC, sometimes under the name "partial self borrow". I would love to see that feature in stable someday, and there was some push for deciding on a syntax, but it seems ultimately noone has taken ownership to drive this to completion. Adding the syntax (any syntax) would be relatively easy and fast to do, the actual NLL changes should be straightforward but I'm not familiar enough with what this would entail.

Yeah, that's exactly what I was looking for, thank you! I knew I was missing something dead simple haha.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.