DEV Community

Eze Sunday Eze
Eze Sunday Eze

Posted on

Rust Lifetimes Simplified

This guide is an attempt to simplify Rust Lifetimes, it's a series and will be divided into several chapters. We'll start with chapter one today.

Chapter 1

Lifetimes and its benefits

Ownership and borrowing

Lifetimes and its benefits

In Rust programming, lifetimes is a critical yet misunderstood concept. Assuming we are not talking about programming, when we talk about lifetimes what comes to mind?

According to Oxford Dictionary, a lifetime is the duration of a person's life or the duration of a thing's existence or usefulness.
Oxford Dictinary

Hold that definition at heart as we go through this guide. That is the starting point to understanding what lifetimes are in Rust.

In Rust, a lifetime is the period during which a reference to data is valid. In other words, lifetimes are a way for the ‘borrow checker’ in the Rust compiler to ensure that references to data are always valid. This helps prevent common bugs, such as dangling references — a "dangling reference" refers to a situation where a reference points to data that has been deleted or deallocated from memory. 

The flowchart below illustrates how a reference is created, or 'borrowed.' The Rust compiler, through its borrow checker, verifies whether the original data that the reference points to is still valid. If the data has been dropped (meaning its lifetime has ended), the compiler will throw a compile-time error, as this would result in a 'dangling reference'. On the other hand, if the data is still valid, you're allowed to continue using the reference.

Flowchart to showcase a basic workflow of a lifetime in Rust

Let’s take a look at a simple code example to further solidify your understanding of the basic concept of lifetimes.

"Example"

Here is the copyable version in case you want to run it quickly!


fn main() {

    let ref_to_text; // `ref_to_text` lifetime starts. ──────┐

   { // - new scope                                          │

        let text = String::from("Hello, Rust!"); // `text` lifetime starts. ──┐

        ref_to_text = &text; // `ref_to_text` now references `text`.          │

    } // `text` lifetime ends, `text` is dropped. ────────────────────────────┘

    println!("{}", ref_to_text); // ??? `ref_to_text` is now a dangling reference.

} // `ref_to_text` lifetime ends. ─────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

The code snippet above, which is annotated with helpful comments, starts with the declaration of the variable ref_to_text on line 3. No value is assigned to ref_to_text yet.

On line 4, we enter a new scope defined by the set of curly braces {}. Scopes can be thought of as different realms where each variable declared within them exists and is valid until the scope ends - indicated by a closing curly brace }.

{ // - new scope

}
Enter fullscreen mode Exit fullscreen mode

Within this scope, ref_to_text is assigned a reference to the variable text, which is also declared within this scope.


{ // - new scope

  let text = String::from("Hello, Rust!"); // `text` lifetime starts. ──┐

  ref_to_text = &text; // `ref_to_text` now references `text`.

}
Enter fullscreen mode Exit fullscreen mode

At this point, ref_to_text holds a reference to the text value stored in memory. However, trouble begins on line 7, when the inner scope ends and the text value is dropped or deallocated.

Then, on line 8, we attempt to print the value referenced by ref_to_text. Unfortunately, this value was dropped at the end of the previous scope on line 7, resulting in a compile-time error. Essentially, we're attempting to access a value that no longer exists in memory.

The error message generated by the Rust compiler would be something like this:

|

4 |         let text = String::from("Hello, Rust!");

  |             ---- binding `text` declared here

5 |         ref_to_text = &text;

  |                       ^^^^^ borrowed value does not live long enough

6 |     }

  |     - `text` dropped here while still borrowed

7 |     println!("{}", ref_to_text);

  |                    ----------- borrow later used here
Enter fullscreen mode Exit fullscreen mode

The error message is quite descriptive: the reference &text that we borrowed and assigned to ref_to_text does not live long enough. It was dropped or deallocated on line 7, so it can't be used on line 7 or anywhere else outside the scope.

Rust promises memory safety, right? It leverages Rust ownership and lifetimes to guarantee memory safety without needing a garbage collector. 

A garbage collector is a form of automatic memory management used in many programming languages. Its primary role is to automatically reclaim the memory that an application no longer needs, effectively protecting against common programming errors like memory leaks. While a garbage collector can simplify memory management, it often comes with some performance overhead since the garbage collector periodically checks and frees up memory space during the execution of the program.

Rust, however, takes a different approach to memory management that does away with a garbage collector. It achieves this by leveraging the ownership and lifetimes models to ensure that all references to data are valid for as long as those references exist — essentially ensuring that the memory is properly allocated and deallocated, and preventing invalid memory access.

Lifetimes in Rust can be either explicit or implicit. Explicit lifetimes, marked in the code using a special syntax like 'a, tell the Rust compiler precisely how long a particular reference should stay valid. On the other hand, Rust can also infer lifetimes implicitly in many scenarios, saving you from having to annotate your code. We’ll discuss implicit and explicit lifetimes in more detail in Chapter 3.

A special mention goes to the 'static' lifetime, which represents a reference that remains valid for the entire duration of the program. We’ll discuss more about this in Chapter 5.

Quiz:

What is a lifetime in the context of Rust programming?

What is a 'dangling reference'?

How does Rust's 'borrow checker' prevent the creation of 'dangling references'?

Exercise:

Write a simple Rust program that correctly uses a reference within its lifetime.

Modify that program to create a 'dangling reference' and see if you can predict the error message that the Rust compiler will return.

Ownership and Borrowing

Ownership and Borrowing are two core concepts in Rust that go hand in hand with Lifetimes. They are Rust's way of managing memory safety without a garbage collector, by setting clear rules for resource allocation and deallocation.

Ownership

In Rust, every value has a variable that's called its 'owner'. There can only be one owner at a time, and when the owner goes out of scope, the value will be dropped, or in other words, cleaned up. 

Consider this simple example:


fn main() {

    let s = "hello"; // s is now the owner of the value "hello"

} // s goes out of scope here, and "hello" is dropped

Enter fullscreen mode Exit fullscreen mode

Here, the variable s is the owner of the value "hello". When s goes out of scope at the end of main, the value "hello" is automatically dropped.

Borrowing

Often, we need to access a value without taking ownership. This is where borrowing comes in handy. Borrowing is when you take a reference to a value without taking ownership. When you're finished with the reference, the borrow ends, and the value is still owned by the original owner.

Look at this example:


fn main() {

    let s = String::from("hello"); // s owns "hello"

    let len = calculate_length(&s); // s is borrowed by calculate_length

    println!("The length of '{}' is {}.", s, len);

} // s goes out of scope and is dropped here, but it wasn't dropped before because calculate_length only borrowed s

fn calculate_length(s: &String) -> usize {

    s.len()

} // s is no longer in scope here, so the borrow ends

Enter fullscreen mode Exit fullscreen mode

In this case, calculate_length borrows s but doesn't own it. Once calculate_length finishes, s is no longer borrowed, and the original owner can continue using it.

Quiz:

What is ownership in Rust?

How does borrowing work in Rust?

How do ownership and borrowing relate to lifetimes in Rust?

Exercise:

Write a Rust function that borrows a value without taking ownership of it.

Modify that function to take ownership of the value, and observe how that changes the function's behavior.

Finally, while Ownership and Borrowing are not the central focus of this guide, they form the foundation upon which Lifetimes operate. The rules of Ownership dictate who manages data. Borrowing allows safe access to data without transferring ownership. And Lifetimes ensure that these borrows are valid while in use.

In the rest of this guide, we’ll delve into lifetimes in detail. So you can be an even more confident Rust developer.

Want to be the first to know when I publish the next chapter?

Sign Up for my newsletter at https://Leandev.space

Happy hacking!

Note: First appeared on ezesunday.com

Top comments (0)