"Does not live long enough" for borrow()

I have a trait that specifies a ClientRepository:

pub trait ClientRepository {
    fn by_id(&self, id: &str) -> Result<&Client, String>;
    fn save(&self, client: Client);
    fn next_identity(&self) -> String;
}

One of the implementations of this trait is:

pub struct InMemoryClientRepository {
    clients : RefCell<HashMap<String, Client>>
}

impl InMemoryClientRepository {
    pub fn new() -> InMemoryClientRepository {
        let mut clients : HashMap<String, Client> = HashMap::new();

        let client1 = Client::new(String::from("1"), String::from("Client number 1"));
        let client2 = Client::new(String::from("2"), String::from("Client number 2"));

        clients.insert(client1.id(), client1);
        clients.insert(client2.id(), client2 );

        return InMemoryClientRepository {
            clients: RefCell::new(clients)
        }
    }
}

impl ClientRepository for InMemoryClientRepository {
    fn by_id(&self, id: &str) -> Result<&Client, String> {
        let id_string = String::from(id);
        let clients_ref = self.clients.borrow();

        let client = clients_ref.get(&id_string);

        match client {
            Some(c) => Ok(&c),
            None => Err(String::from("No client found for given ID"))
        }
    }

    fn save(&self, client: Client) {
        self.clients.borrow_mut().insert(client.id(), client);
    }

    fn next_identity(&self) -> String {
        let size = self.clients.borrow().len();

        String::from(size.to_string())
    }
}

I had to use RefCell to allow dynamic mutability for the HashMap. However, every time I compile, I get the following error:

error[E0597]: `clients_ref` does not live long enough
  --> src/infrastructure/domain/repositories.rs:31:22
   |
31 |         let client = clients_ref.get(&id_string);
   |                      ^^^^^^^^^^^ borrowed value does not live long enough
...
37 |     }
   |     - borrowed value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the method body at 27:5...
  --> src/infrastructure/domain/repositories.rs:27:5
   |
27 | /     fn by_id(&self, id: &str) -> Result<&Client, String> {
28 | |         let id_string = String::from(id);
29 | |         let clients_ref = self.clients.borrow();
30 | |
...  |
36 | |         }
37 | |     }
   | |_____^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.

I have tried already wrapping both the HashMap and the Client into an Rc. but I cannot get rid of the error. Please, could you advice me in what am I doing wrong?

RefCell<T> does borrow-checking at run time by using counters that are incremented when you call borrow and decremented when you drop the Ref<T> that it returns.

This means that you have to keep that Ref<T> around for as long as the contents of the RefCell are accessible. So the by_id method can’t return a &Client by itself. It will need to return a Ref<Client> (or some type that contains a Ref<Client>). Or alternately, you could change by_id to take a callback that gets access to the Client only while the HashMap is borrowed:

fn by_id<F>(&self, id: &str, callback: F) where F: FnMut(&Client);

(Or if you can get rid of the RefCell and change save and next_identity to take &mut self then you could return a regular reference from by_id.)

1 Like

Thank you for the explanation, you gave me something to work on. Now I understand better RefCell.

Unfortunately, I am trying to follow a Domain Driven Design aproach, and I would not like to modify the trait signature: one of the implementations of the trait (this one) would be satisfied using either the callback or the mut, but if instead of a Hashmap I would like to persist the Client into a database in another implementation of the trait, the mut would not be necessary at all. I.e. I am trying to not acommodate the trait to the needs of each one of its implementations.

If you have any more feedback after reading this it will be welcomed :slight_smile:

You will likely run into similar problems with an implementation backed by a persistent on-disk database. If the signature of by_id returns an &Client , then in any scope where this &Client exists it must point to a valid Client in memory. And because the other methods do not require exclusive (&mut) access to the ClientRepository, there can always be existing &Client references still alive when any of those methods are called; there’s no point where it’s safe to clean up or invalidate Client values that have been loaded from the database into memory.

Using &mut to enforce exclusive access at compile time (even if you don’t need to mutate the data in your Rust program’s memory) could provide one way around this. Using some form of smart pointer to do clean-up at run-time (similar to std::cell::Ref) would be another, though it can be tricky to do this in a way that’s as generic as you want (without experimental features like Associated Type Constructors).

Another option might be to return the Client by value rather than by reference. If Client is large or otherwise expensive to clone, then you could store and return Arc<Client> or Rc<Client> pointers instead.

2 Likes

In Rust, & is temporary borrow limited to a certain scope. That is not equivalent of returning things by reference in C++ or Java, so use of & in typical programming patterns from other programming language may be inappropriate and implicitly promise usage restrictions that these patterns are not supposed to give.

For OOP-like design, you’ll usually need to use Rc<RefCell<Client>> or Arc<Mutex<Client>> pretty much everywhere.

Encapsulation of lifetimes, and types related to objects’ lifetime like Arc/Rc, usually doesn’t make much sense in Rust, because the compiler enforces proper lifetimes and mutability, so that becomes an interface, not an implementation detail.

So if your repository returns a Client object for anyone to use as long as they need, and the returned object is shared, you can’t hide that fact. You have to make the function signature allow for that.

Thank you very much for all your help. At the end, and after struggling some time, I decided to change &Client into Client. The method became:

fn by_id(&self, id: String) -> Result<Client, String> {
        let id_string = String::from(id);

        let client = self.clients.borrow().get(&id_string).cloned();

        match client {
            Some(c) => Ok(c),
            None => Err(String::from("No client found for given ID"))
        }
    }
1 Like

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