Rust lifetime syntax when borrowing variables

New to Rust and trying to teach myself, etc. I'm stuck on a lifetime issue. The closest question I could find that was already posted was:

Argument requires that _ is borrowed for 'static - how do I work round this?

The small project I'm playing around with defines two structs, Agent and Item.

The Agent struct contains, among other things, this line:

pub inventory: HashMap<String, &'static Item>,

Additionally, I've implemented this code:

impl Agent {
    
pub fn take_item(&mut self, item: &'static Item) -> std::result::Result<(), TestError> {
        if item.can_be_taken {
            if self.can_reach_item(item) {
                self.inventory.insert(item.name.to_string(), item);
                return Ok(());
            } else {
                return Err(ItemOutOfReachError {});
            }
        } else {
            return Err(ItemCannotBeTakenError {});
        }
    }
}

I've written a unit test which includes this line

let result = test_agent.take_item(&test_item);

I know there is a mistake somewhere because the compiler tells me:

  --> src/agent.rs:57:47
   |
57 |             let result = test_agent.take_item(&test_item);
   |                          ---------------------^^^^^^^^^^-
   |                          |                    |
   |                          |                    borrowed value does not live long enough
   |                          argument requires that `test_item` is borrowed for `'static`
...

I am passing the test_item as a reference to take_item() (or rather, take_item() is "borrowing" test_item if I'm using the jargon correctly). This seems to be the source of my error, but in the earlier post I linked, the author was able to solve the issue by adjusting the lifetime of the Option<> which contains the reference, as I understand it. In my example, I'm just using a bare reference to test_item. Is containing it, like the other author did, the recommended approach?

And the 'static lifetime means that the test_item will essentially live for as long as the unit test is running?

I think my primary question boils down to, with what syntax must take_item() borrow the test_item in order for my code to be correct? And am I even thinking about this correctly?

Thanks for any advice.


Solution 1:

The main problem in your code is that you used the 'static lifetime in your struct.

I will try to explain what lifetimes are, how do they work and why you are facing this error. I warn you that this will be long and probably you will have doubts so at the end I am going to link a really good video where lifetimes are explained wonderfully.

What are lifetimes?

First of all, I will assume you have looked up some basic Rust terminology such as borrowing and moving and how rust's ownership works. If not I highly recommend you read the Understanding Ownership section in the Rust Book.

So basically a lifetime is used by the rust compiler to define how long does a reference live in your program. Let's say we have the following code (taken from the book):

{
    let r;
    {
        let x = 4;
        r = &x;
    }
    println!("r: {}", r);
}

The above code will not compile because the reference to x outlives the variable. This means that while x will be dropped in when the end of the inner scope is reached, you are saving a reference to it in the outer scope. So when you reach the println! basically you have a reference to a variable that no longer "exists".

An easier way to understand this is to say that r lives longer than x and so you cannot save a reference of x into r because at some point x will have died and the reference stored in r will be invalid.

  • r has a longer lifetime than x
  • r outlives x

In order to keep track of these errors, the rust compiler makes use of identifiers. These can have almost any name preceded by a '. So 'a is a valid lifetime as such as 'potato. All references in Rust have a lifetime which is determined by how long they live (the scope they are in).

For example, in the code above there are two lifetimes:

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

So as 'a outlives 'b you cannot save a &'b reference into the 'a lifetime.

Lifetime elision

Now you may be asking yourself why you don't see lifetime annotations often, this is called lifetime elision and is a process in which the rust compiler does a little work for you so that you can focus on programming instead of annotating all the references in your program. For example, given the following function:

fn takes_a_ref(name: &str) {
    ...
}

The rust compiler will define a new lifetime name for the scope corresponding to the brackets of the function automatically. You could annotate it using almost any name but the compiler uses the letters of the alphabet to define new lifetime names for the sake of simplicity. Let's say that the compiler chooses the letter 'a then this function will be annotated automatically as:

fn takes_a_ref<'a>(name: &'a str) {
    ...
}

This means that the lifetime of takes_a_ref is called 'a and that the reference you pass to takes_a_ref must point to a variable that lives at least as long as 'a (the function).

The compiler does this automatically for you most of the time, but other times you must define the lifetime manually such as in structs.

pub struct MyStruct {
    pub field: &str
}
// Does not compile

Should be annotated as:

pub struct MyStruct<'a> {
    pub field: &'a str,
}

Special lifetime names

You've probably noted that I have been talking about almost any name when referring to the possibilities of naming lifetimes. This is because there exist a couple of reserved lifetime names that have special meanings:

  • 'static
  • '_

The 'static lifetime is a lifetime that corresponds to the entire lifetime of the program. This means that in order to obtain a reference with an 'static lifetime the variable it points to must life from whenever the program is started until it is finished. An example is const variables:

const MY_CONST: &str = "Hello! 😃"; // Here MY_CONST has an elided static lifetime

The '_ lifetime is called the anonymous lifetime which is only a marker to point that there has been a lifetime elision in a variable. It will be replaced by the compiler compile time it only serves clarification porpoises.

What is wrong with your code?

So you have encountered the following situation:

  1. You have created a struct called Agent which contains a HashMap.
  2. This HashMap contains an owned String and a reference to an Item.
  3. The compiler tells you that you must specify the lifetime of the Item as the compiler does not elide lifetimes in structs.
  4. You have annotated Item with the 'static lifetime.
  5. Then you are forced to pass a 'static reference in the take_item function as in some times you may save the item inside the struct's HashMap which now requires a 'static lifetime of Item.

This now means that the reference to Item must point to an instance of Item that lives for the entirety of the program. For example:

fn function() {
    let mut agent = Agent::new();
    let my_item = Item::new();
    let result = agent.take_item(&item);
    ...
}

fn main() {
    function();
    // Do some other stuff. The scope of 'function' has ended and the variables dropped but the program has not ended! 'my_item' does not live for the entirety of the program.
}

You don't need my_item to live as long as the entirety of the program you need my_item to live as long as the Agent does. This is for any reference that will be stored inside the Agent, it just needs to live as long as it does the Agent.

The solution (Option 1)

Annotate Agent with a lifetime that is not the 'static lifetime, for example:

pub struct Agent<'a> {
    pub items: HashMap<String, &'a Item>,
}

impl <'a> Agent<'a> {
    pub fn take_item(&mut self, item: &'a Item) -> std::result::Result<(), TestError> {
        ...
    }
}

This means that as long as the instance where the reference points lives as long as or more than the specific instance of Agent where it's stored there will be no problems. In the take_item function you specify:

The lifetime of the variable where the reference points must be equal or longer than this Agent.

fn function() {
    let mut agent = Agent::new();
    let my_item = Item::new();
    let result = agent.take_item(&item);
    ...
}

fn main() {
    function();
    // No problem 🎉🥳
}

Will now compile fine.

Take in mind that you may have to start annotating functions in order to coerce the item to live as long as the Agent.

Read more about lifetimes in the book.

The solution (Option 2)

Do you actually need the item to be stored as a reference inside the Agent? If the answer is NO then you can just pass the ownership of the Item to the agent:

pub struct Agent {
    pub items: HashMap<String, Item>,
}

In the implementation the function lifetime is elided automatically to live for as long as the function:

pub fn take_item(&mut self, item: &Item) {
    ...
}

So this is it. Here you have a video from the YouTube channel Let's Get Rusty where lifetimes are explained.