3

I want to manage a collection of objects in another object but I can't predict the lifetime of the elements in this collection.

I found this example in Syntax of Rust lifetime specifier that demonstrates what I can't do:

struct User<'a> {
    name: &'a str,
}

// ... impls omitted

struct ChatRoom<'a> {
    name: &'a str,
    users: HashMap<&'a str, User<'a>>,
}

ChatRoom holds a map of Users. Each User is a copy although the name within User is a shared reference. The User and the ChatRoom have an explicit lifetime so when they are joined the compiler enforces that Users must live longer than the ChatRoom they're going into.

But what if my User was created after the ChatRoom? I can't use lifetimes because the compiler will complain. What if I delete a User before the ChatRoom? I can't do that either.

How can the ChatRoom hold Users who might be created after it or destroyed before it? I vaguely suspect that something could be done with boxes to implement this but Rust's box documentation is quite poor so I am not certain.

Community
  • 1
  • 1
locka
  • 5,809
  • 3
  • 33
  • 38
  • No because I don't want to clone state information in this object. Think of User above - the name is a reference. In my case I have fields which can change inside my User so I don't want multiple copies floating around where the info gets stale. I could of course hold some kind of token / id to the user in the map and look it up when I need to but I'd rather just have a straight reference. – locka May 28 '16 at 15:27

1 Answers1

5

In Rust, some types come in pairs: a borrowed and an owned counterpart. For strings, the borrowed version is &'a str and the owned version is String. Owned versions don't have a lifetime parameter, because they own all their data. That doesn't mean they don't contain pointers internally; String stores its data on the heap, and the actual String object only contains a pointer to that data.

By using String instead of &'a str, you can avoid issues with the order of construction, because you can move owned data around freely (so long as it isn't borrowed elsewhere). For example, when you create a User, you first need to create a String, which you would then move into the new User, and finally you then move the User into the ChatRoom's HashMap.

struct User {
    name: String,
}

struct ChatRoom {
    name: String,
    users: HashMap<String, User>,
}

However, since you need shared references, then you need to wrap the String in a type that provides that functionality. If you are writing a single-threaded program, you may use Rc for this. If you need to access these references from multiple threads, then Rc won't work; you need Arc instead.

struct User {
    name: Rc<String>,
}

struct ChatRoom {
    name: String,
    users: HashMap<String, User>,
}

In order to create a new Rc<String> that points to the same string, you simply call the clone() method on the first Rc<String>.


Now, the String in an Rc<String> is immutable, because Rc doesn't provide any way to mutate its value (without consuming the Rc). If you need that capability, then you need to pair this with RefCell (or either Mutex or RwLock in a multi-threaded program).

struct User {
    name: Rc<RefCell<String>>,
}

struct ChatRoom {
    name: String,
    users: HashMap<String, User>,
}
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155