Go to course homepage
    Memory and Lifetimes

    Lifetimes

    Lifetimes are, in my opinion, one of the biggest sources of confusion to folks starting out writing Rust. They also tend to look scary when reading some Rust code for the first time. I think this is because of two reasons:

    • they look very terse syntactically
    • they are a pretty foreign concept to folks coming from languages like JavaScript

    Before we talk about what they really are and how to use them, let's walk through an example to encounter them: compiler error driven learning, if I may.

    Earlier in the course, we learnt how to define a struct. We'll replace the age field in it to location for these examples and write a simple main function to print some text.

    struct Person {
        name: String,
        location: String,
    }
    
    
    fn main() {
        let me = Person {
            name: String::from("Sid"),
            location: String::from("London"),
        };
    
    
        let jon = Person {
            name: String::from("Jon"),
            location: String::from("St. Petersburg"),
        };
    
    
        println!("Hello, {} from {}!", me.name, me.location);
        println!("Hello, {} from {}!", jon.name, jon.location);
    }
    

    Everything above should look pretty familiar so far. We've created a few String instances since our struct takes an owned String.

    Now, Jon just happened to move to London. So if I edit his struct it'll look something like this.

    fn main() {
        let me = Person {
            name: String::from("Sid"),
            location: String::from("London"),
        };
    
    
        let jon = Person {
            name: String::from("Jon"),
            location: String::from("London"),
        };
    
    
        println!("Hello, {} from {}!", me.name, me.location);
        println!("Hello, {} from {}!", jon.name, jon.location);
    }
    

    What sticks out above is how we're creating two identical String instances with the contents, "London". One might be tempted to move that over to a single variable. Let's try that.

    struct Person {
        name: String,
        location: String,
    }
    
    
    fn main() {
        let london = String::from("London");
        let me = Person {
            name: String::from("Sid"),
            location: london,
        };
    
    
        let jon = Person {
            name: String::from("Jon"),
            location: london,
        };
    
    
        println!("Hello, {} from {}!", me.name, me.location);
        println!("Hello, {} from {}!", jon.name, jon.location);
    }
    

    This doesn't compile and if you worked through the last couple of lessons, you'll know why! When we initialise me, when move ownership of the london variable into the struct.

    error[E0382]: use of moved value: `london`
      --> src/main.rs:15:19
       |
    7  |     let london = String::from("London");
       |         ------ move occurs because `london` has type `String`, which does not implement the `Copy` trait
    ...
    10 |         location: london,
       |                   ------ value moved here
    ...
    15 |         location: london,
       |                   ^^^^^^ value used here after move
    
    
    

    The compiler does give us a really nice error message saying the same thing.

    If String implemented the Copy trait, this would just work because it would be automatically copied (cloned) under the hood instead of moved.

    The move occurs because String doesn't. More on traits soon.

    Okay, let's try to fix this with using references in the struct instead.

    struct Person {
        name: String,
        location: &String,
    }
    
    
    fn main() {
        let london = String::from("London");
        let me = Person {
            name: String::from("Sid"),
            location: &london,
        };
    
    
        let jon = Person {
            name: String::from("Jon"),
            location: &london,
        };
    
    
        println!("Hello, {} from {}!", me.name, me.location);
        println!("Hello, {} from {}!", jon.name, jon.location);
    }
    

    What we've done now is that instead of including an owned String, the Person struct has a field location which is a reference to a String instead, &String. This should work and we should be able to use the same variable with two immutable references as we've learned, right?

    Go ahead and try compiling it.

    Spoilers below:

    error[E0106]: missing lifetime specifier
     --> src/main.rs:3:15
      |
    3 |     location: &String,
      |               ^ expected named lifetime parameter
      |
    help: consider introducing a named lifetime parameter
      |
    1 ~ struct Person<'a> {
    2 |     name: String,
    3 ~     location: &'a String,
      |
    

    What I love about the error message is that it actually fixes our issue entirely. So let's edit it in first and then understand what just happened.

    struct Person<'a> {
        name: String,
        location: &'a String,
    }
    
    
    fn main() {
        let london = String::from("London");
        let me = Person {
            name: String::from("Sid"),
            location: &london,
        };
    
    
        let jon = Person {
            name: String::from("Jon"),
            location: &london,
        };
    
    
        println!("Hello, {} from {}!", me.name, me.location);
        println!("Hello, {} from {}!", jon.name, jon.location);
    }
    

    This compiles perfectly with the output:

    Hello, Sid from London!
    Hello, Jon from London!
    

    Now, let's understand what just happened.

    What is a Lifetime?

    We learnt about the Rust compiler's borrow checker before and how it prevents uncertainty in your programs by disallowing mutations by more than one code path at a time. It also ensures that there aren't immutable references along with mutable ones so that one code path doesn't try to read some memory while another writes to it.

    There's one more aspect of memory safety that the Rust compiler tries to ensure and this is the lifetime of a value. Typically the lifetime of a value is its surrounding scope.

    fn main() {
        let x = 1;
        {
            let y = 2;
        }
    }
    

    The lifetime of x in the snippet above is till the end of the main function and the lifetime of y is till the end of the block scope it's in, that is the closing }. The word lifetime here refers to how long the memory referenced by the variable lives. In the snippet above, the compiler was able to infer this.

    Lifetime annotations

    Sometimes the compiler isn't able to infer the lifetime of a value though. Let's go back to our Person struct. Before we added the magic 'a, the compiler printed an error message saying error[E0106]: missing lifetime specifier.

    This is because the compiler was not able to infer a certain lifetime and needed your help.

    When we changed the type of the location key in the Person struct to a &String (a reference to a String and not a String itself), the lifetime of an instance of this struct is no longer just a function of the scope it is created in.

    It is now also a function of the scope that the value referenced in location is created in. This relationship is what we're trying to signify with a lifetime annotation.

    struct Person<'a> {
        name: String,
        location: &'a String,
    }
    

    A lifetime annotation looks like 'a and it is typical in Rust land to use lower case single alphabets for lifetime annotations. What the above snippet is telling us is that the Person struct's lifetime is now a function of its own scope and also the lifetime what location references. An instance of the struct itself cannot outlive a value it is referencing.

    An important point here to remember is that you are not changing behaviour or the lifetime of a value by annotating it. You are simply giving the compiler information for it to then make assertions based on.

    Multiple lifetimes

    This is where things start to get interesting. When your function or struct depends on multiple references, you'll likely have to annotate multiple lifetimes and the relationships between them. Let's refactor our code from before a little.

    So far we were storing the name of a city in the location field on Person. We'll swap that out for a HashMap. A HashMap is a data structure in the Rust std library that lets you store key value pairs.

    Think of it as a JavaScript Map. They both have:

    • key value pairs where a key must be unique

    However, like all things in Rust and unlike the JavaScript Map, a HashMap is typed. All keys and values must conform to their specific types respectively.

    Let's start with adding a use std::collections::HashMap; at the top of our file so that HashMap is in scope. We'll initialise our HashMap using its ::from function which takes a slice of tuples (of key and value pairs):

    use std::collections::HashMap;
    
    
    fn main() {
        let airport_codes = HashMap::from([
            ("PIE", "St. Petersburg"),
            ("LHR", "London"),
            ("BOM", "Mumbai"),
        ]);
    }
    

    Now, we'll be able to lookup values in airport_codes using the get function on a HashMap. The get function returns an enum called Option which we mentioned earlier. Let's take a quick detour to understand what Option is.

    The Option enum has two possible values: Some and None. In JavaScript, to represent an optional value, we set a variable to undefined or null and then are forced to have conditionals everywhere to ensure a value is set.

    In Rust, the optionality of a value is built into the type system. Think of the Option enum as a wrapper for an optional value. When a value is present, we wrap it using one of the Option enum types: Some and when it is not set, we use None. Let's take a look at an example:

    struct Person {
        name: Name,
        age: u8,
    }
    
    
    struct Name {
        first_name: String,
        middle_name: Option<String>,
        last_name: String,
    }
    fn main() {
        let john = Person {
            name: Name {
                first_name: String::from("John"),
                middle_name: Some(String::from("Richard")),
                last_name: String::from("Doe"),
            },
            age: 30,
        };
        let mary = Person {
            name: Name {
                first_name: String::from("Mary"),
                middle_name: None,
                last_name: String::from("Poe"),
            },
            age: 30,
        };
    }
    

    Our Person struct now uses another struct for its name field, our brand new Name struct. Now because we want to make the middle_name optional, we've wrapped a String with Option so its type is now Option<String>. Valid values for this are either a value wrapped in Some or None as the examples illustrate.

    Some and None are possible values for the Option enum so really they're Option::Some and Option::None. We're able to get away with just Some and None because Rust includes them in scope by default in what is called its prelude.

    Okay, now that we understand what Option is, let's get back to our HashMap. When looking up a value from a HashMap, if a value isn't found for a key, the get function returns None, otherwise a value wrapped in Some.

    use std::collections::HashMap;
    
    
    fn main() {
        let airport_codes = HashMap::from([
            ("PIE", "St. Petersburg"),
            ("LHR", "London"),
            ("BOM", "Mumbai"),
        ]);
        println!("{:?}", airport_codes.get("PIE"));
        println!("{:?}", airport_codes.get("ADD"));
    }
    

    You'll notice the snippet above prints:

    Some("St. Petersburg")
    None
    

    We get a None for our second call because our airport_codes HashMap doesn't include the key, "ADD".

    So back to our refactor, let's use &str instead of references to String in our Person struct to clean up the initialisation code:

    use std::collections::HashMap;
    
    
    struct Person<'a> {
        name: &'a str,
        location: &'a str,
    }
    
    
    fn main() {
        let airport_codes = HashMap::from([
            ("PIE", "St. Petersburg"),
            ("LHR", "London"),
            ("BOM", "Mumbai"),
        ]);
    
    
        let me = Person {
            name: "Sid",
            location: "LHR",
        };
    }
    

    Notice how both the name and location fields have the same lifetime annotation. They were created in the same scope and hence have the same lifetime.

    Finally, let's write a function to look up a city name from an airport code and use it for our greeting.

    fn get_city(code: &str, airport_codes: &HashMap<&str, &str>) -> &str {
        airport_codes
            .get(code)
            .expect("We don't know this location!")
    }
    

    Take a moment to read through the function above. It takes two arguments, a code which is an &str and an immutable reference to a HashMap of type, <&str, &str> since both the key and the value are &str. The other interesting bit is that we call expect on the Option that is returned by get. expect is function used to unwrap an Option and returns the value unwrapped from a Some and panics if the returned value is None (with the message passed in as an argument to expect).

    You don't want to be using expect in production unless there is a situation where your program must need stop running and panic if an Option is None. Ideally, we would be handling this case here and returning an error but we're keeping it simple to focus on the topic at hand.

    All together, we now have:

    use std::collections::HashMap;
    
    
    struct Person<'a> {
        name: &'a str,
        location: &'a str,
    }
    
    
    fn get_city<'a>(code: &str, airport_codes: &HashMap<&str, &str>) -> &str {
        airport_codes
            .get(code)
            .expect("We don't know this location!")
    }
    
    
    fn main() {
        let airport_codes = HashMap::from([
            ("PIE", "St. Petersburg"),
            ("LHR", "London"),
            ("BOM", "Mumbai"),
        ]);
    
    
        let me = Person {
            name: "Sid",
            location: "LHR",
        };
    
    
        println!(
            "Welcome, {} from {}!",
            me.name,
            get_city(&me.location, &airport_codes)
        );
    }
    

    This doesn't compile and throws the following:

    error[E0106]: missing lifetime specifier
     --> src/main.rs:8:65
      |
    8 | fn get_city(code: &str, airport_codes: &HashMap<&str, &str>) -> &str {
      |                   ----                 --------------------     ^ expected named lifetime parameter
      |
      = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `code` or one of `airport_codes`'s 3 lifetimes
    help: consider introducing a named lifetime parameter
      |
    8 | fn get_city<'a>(code: &'a str, airport_codes: &'a HashMap<&str, &str>) -> &'a str {
      |            ++++        ++                      ++                          ++
    

    Now, we've seen this error before and we could go ahead and do what the compiler says and that'll fix it.

    Let's understand what fn get_city<'a>(code: &'a str, airport_codes: &'a HashMap<&str, &str>) -> &'a str is:

    • The function get_city takes code which has a lifetime of 'a and
    • airport_codes which has a lifetime of 'a as well and
    • therefore the returned value will also have a lifetime of 'a

    This happens to be correct here because both the passed in code and the HashMap do indeed have the same lifetime.

    What if they didn't? What if the HashMap was created earlier and lived longer whereas the Person was just initialised on demand via an API call or something?

    Then, we'd need two separate lifetime annotations.

    fn get_city<'a, 'b>(code: &'a str, airport_codes: &'b HashMap<&str, &str>) -> &'b str {
        airport_codes
            .get(code)
            .expect("We don't know this location!")
    }
    

    Here the return value has the same lifetime as the HashMap which makes sense since that's where the value is coming from.

    Outlives

    Sometimes, it might be helpful to annotate relationships between certain lifetimes. We won't dive too deep into this just about now, but if we wanted to annotate the fact that we expect the HashMap to "outlive" the passed in code, we'd write it with a where clause:

    fn get_city<'a, 'b>(code: &'a str, airport_codes: &'b HashMap<&str, &str>) -> &'b str
    where
        'b: 'a,
    {
        airport_codes
            .get(code)
            .expect("We don't know this location!")
    }
    

    This is representing the fact that 'b outlives 'a.

    'static

    Finally, a special lifetime annotation we should talk about is the 'static lifetime. The 'static means that the value has the lifetime of the running program itself and this is useful for constants or any values that we expect to be inlined in our binaries.

    The most typical of these are variables declared with the static keyword.

    static VERSION: u8 = 1;
    fn main() {
        println!("v{}", VERSION);
    }
    

    Here VERSION has the 'static lifetime. You don't need to annotate it in this case because it is inferred but if it were used as a reference in a struct for instance, you could annotate it with the 'static lifetime.

    Remember: the 'static lifetime outlives all other lifetimes because it is valid till the very end of the execution of your code.

    Enjoyed this lesson and want to dive into the rest?
    Enroll now