DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 966,904 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Lifetimes in Rust
Ay oh
Ay oh

Posted on

Lifetimes in Rust

When you learn about the borrow checker, there's a new concept that you need to get familiar with and that's Lifetimes. I mentioned it briefly in my post on the borrow checker. Now, what are lifetimes?

Lifetimes are how long a reference can live in a program. What does this mean?
Let's look at the example code below.


{
let ray; // we can do this for now, but if we run it in our code, it gives a compilation error, since rust doesn't allow null values
{
let rays= "rays";  //This is how long "rays" live
ray = &rays; //this is the end of the scope
}
println!("The sun {}", &ray) //outside the scope, into the outer scope.
}

Enter fullscreen mode Exit fullscreen mode

This will compile to an error that says "ray" doesn't live long enough. If you come from JavaScript you'll think, okay, this is similar to closures. I'm here to tell you that it's not. The concept of lifetimes is broader than you think.

Whenever we are using references, underneath the hood, they have smart pointers that point to the address of the value. This is how you can use references. If they point to an address that has no value the program will give a compilation error. This is why lifetimes are important.

Lifetimes can be used in functions⎯ they are annotated by 'a or 'b or any letter with the apostrophe. It is important to know where Rust data types are stored when dealing with lifetimes. Some data types are stored on the stack while some are stored on the heap. Whenever a value is initialised, it is allocated a pointer that will be used to get the address of the value, which can be used as a reference. Now the issue is when you are referencing values, they have to live long enough.

Let's see how they can be used and why it's useful to rust developers.

In functions

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

// 'a lifetime syntax

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { //parameters are string references with lifetimes. This is also used in the return type of the function.
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Enter fullscreen mode Exit fullscreen mode

When you are using lifetimes in functions, it's better to annotate the same lifetime to the parameters and also in the result type.

It might sound a bit confusing. But I want you to picture this: think about when you login into an application, under the hood. You are given a token⎯all that good stuff⎯ and you can keep logging in for as long as the token is still valid. This is similar to the way lifetimes are in rust. Whenever you are using references for some logic, you have to tell the compiler that it lives long enough by annotating the syntax, so when your code is compiling, it will check if the variable you are referring to is still valid(exists).

Why do we need this?
When a program(e.g a function) ends, memory is freed too. This is part of the role of the borrow checker. Lifetimes are needed to explicitly tell the compiler that your reference will be valid. The borrow checker does all the validations in compile-time so if you annotate a lifetime on a reference that doesn't live long enough, you'll get a compilation error.

Another way to use it is in structs.

In Structs

#[derive(Debug, Deserialize)] //this is an attribute, this type enables us to print values to the console when they don't have the display trait
struct Car<'a>{
color: &'a str //expecting a reference to a string slice
}


let car: (&str, &str)= ("toyota", "blue"); // a tuple of string slices
let new_car = Car {
color: &car.1, //this is how you reference values in a tuple
};

println!("{:#?}", new_car.color);
Enter fullscreen mode Exit fullscreen mode

Whenever you use references in structures that are going to be used in logic, you have to specify lifetimes. That is how the compiler is able to keep track of the values in memory.

You can also use lifetimes in methods.

In methods

Methods are kinda similar to functions in the sense that the structure looks like a function but the actions are not.


impl <'a> Car <'a>{
fn driver_liscense (&self) -> &str {
"hello world"
}
}
Enter fullscreen mode Exit fullscreen mode

Lifetimes Elision

Before you run the code block above, you might think to yourself that this code will compile to an error and then you get a bit disappointedπŸ₯²

Lifetime Elision came about when the Rust team found out that Rust programmers kept writing the same patterns of code in lifetime annotations over and over again. So they thought to themselves, "Why not help make it easier; let's cut them some slack." So they made some exceptions where Rust devs don't need to use the lifetime annotation syntax. There are three exceptions (rules).

(Note that according to the rust book, Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes.)

Now let's get into those rules:

The first rule is that each parameter that is a reference gets its own lifetime parameter, i.e., a function with a parameter gets one lifetime parameter, and a function with two parameters gets two separate lifetime parameters.

The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters, this is demonstrated in the example above on lifetimes in functions.

The third rule is if there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters. This is demonstrated in the example of lifetimes in methods.

All these Elision rules apply to only functions and methods.

Finally, there are static lifetimes, they live as long as the entire program. The text of this string is stored directly in the program’s binary, which is always available. String literals are always stored in the binary.

let eyes: &'static str = "I have two eyes.";
Enter fullscreen mode Exit fullscreen mode

That's the basics of lifetimes in rust, it is one of the major mechanisms in the borrow checker, once you get along with it, you'll rarely have to fight with it but you can never avoid itπŸ€ͺ.

This article was previously published by me at Lifetimes in rust

Top comments (0)

🌚 Life is too short to browse without dark mode