Beginner question on lifetimes

I have a problem with lifetimes, it's really messing with my head and am hoping some of you nice Rustaceans can help me. imagine this sort of data structure:

type ListOneItems = Vec<ItemsOne>;
type ListTwoItems = Vec<ItemsTwo>;
type ListOneSubItems = Vec<SubItemsOne>;
type ListTwoSubItems = Vec<SubItemsTwo>;

struct Top {
  first_list : ListOneItems,
  second_list : ListTwoItems,
}

struct ItemsOne {
  name : String,
  subitems : ListOneSubItems,
}

struct SubItemsOne {
  name : String,
  // blah blah blah
}

struct ItemsTwo {
  name : String,
  reference_1 : &ItemsOne,
  reference_2 : &ItemsTwo,
}

struct subItemsTwo {
  name : String,
  reference_1 : &SubItemsOne,
  reference_2 : &SubItemsOne,
}

That is, the top level contains two lists of structs, each of those lists contains a list of structs and the second list (and sub items) contains references to specific items from the first list (so as not to save the same data twice in both lists).

As I try to set this up, I run into all sorts of confusion about how to set up the "lifetimes" properly.

If worse comes to worse, I can tag each of the things in the first list with a generated ID and then store the same ID in the second list, but that seems really inelegant.

This is a self-referential type (because Top.second_list contains references into Top.first_list); these are not possible in safe Rust. The ID solution you mention is a common workaround; another is using Rc<ItemsOne> instead of &ItemsOne.

1 Like

Darn, I hate using IDs for this. Yeah, I understand why it's not considered "safe" rust. Thanks for the quick reply!

Rather than the theoretical example I first posted, here's the actual (start) of the code I was trying to get to work:

struct Bonspiel {
	name : String,
	start : usize,
	rosters : Vec<Roster>,
	draws : Vec<Draw>,
}

struct Roster {
	name : String,
	teams : Vec<Team>,
}

struct Team {
	name : String,
	members : Vec<TeamMember>,
}

enum TeamPosition {
	Skip,
	Vice,
	Second,
	Lead,
	Coach,
	Alternate,
}

struct TeamMember {
	name : String,
	position : TeamPosition,
}

struct Draw {
	number : u8,
	start : usize,
	games : Vec<Game>,
}

struct Game {
	sheet : String,
	opponents : (Opponent, Optional<Opponent>),
}

struct Opponent {
	team : &Team,
	score : u8,
	ends : u8,
	abbreviated_score : u8,
	abbreviated_ends : u8,
}

You'll notice that the "Opponent" struct contain a reference back to a Team, which is held in a list in each Roster struct.

Rosters are used for scheduling, Draws are used for keeping track of the games played by each team.

Ok, IDs it will be.

If this will be mutable, you might be interested in using something like generational_arena instead of Vec for this, which will allow you to remove items without invalidating the old IDs.

Fortunately, it's a read-only operation for the Draw/Game/Opponent side. Only the Roster/Team side needs write access to the team.

I really hate that I have to store some sort of ID rather than a reference to the item I wish to deal with. This means that every time I want to get data from the referenced item, I have to search the tree for the item instead of going straight to it.

Example, using pointers/references:

List 1 -- May change some of the values inside of each item.
    Item 1
    Item 2
    .
    .

List 2 -- Displays a subset of information from each item depending on various criteria
    reference to Item 1
    reference to Item 2

But I can't seem to do this in Rust. instead I have to do something like:

List 1
    ID1, Item 1
    ID2, Item 2
    .
    .
    .

List 2
    ID1
    ID2
    .
    .
    .

and then create a routine "fn find_item(ID) -> Item"

I figure there is a more elegant solution that this, especially as some lists may be thousands of thousands of records, plus the inherent memory inefficiency of storing copies of the ID in so many different places (which has many of the same maintenance issues as pointers/references have).

Generally for ids you can use indexes, they work fine as long as you don't have to modify the list they refer to. This way you can omit storing them (because they're implied by the position in the list) and find_item becomes just an O(1) array access.

The problem with references is that they must remain valid until they're dead. This means that effectively the moment you have a self-reference your struct is immutably borrowed until it's dropped, and you can't move or mutate it.