Variable, References, and Lifetimes in Rust — A Practical Introduction | by Shanmukh Sista | Jun, 2022

Master the Rust basics

Photo by Possessed Photography on Unsplash

In this article, we’ll touch upon some basics for variables, memory, and references in Rust. The goal is to understand why Lifetimes exist in RUST and how can we work with the Rust compiler to write safe code. We’ll see some simple examples that highlight the Rust compiler’s ability to aid us with some errors around references and memory management.

As developers, we are all aware of variables. It’s a universal concept in all programming languages.

A variable is a symbolic name that is given to values ​​present in our program. These may change or remain the same throughout the program. The values ​​assigned to the variables may be stored on the stack or on the heap depending on the data type. Rust provides ways to declare variables using the let keyword. Refer to the examples below:

fn main() { 
let a = 10 ;
let b : u32 = 20 ;
let c = 2999.4 ;
let name = "hello_world" ;
}

Simple types like integers, floats, and doubles are stored on the stack. Whereas complex types like strings, structs, or objects are stored on the Heap.

In either case, memory is assigned during variable initialization. And memory is deallocated whenever the variable is not valid.

Variables act as owners of the data/objects that they are pointing to. It is critical to understand this piece. Once an owner is assigned, the value associated with the variable is valid as long as the variable is valid.

If you’re already familiar with variables and references, refer to this article to understand the rules surrounding ownership

We saw how variables own the data that they point to. We also know that the data is a block of memory that may be on the stack/heap. (Refer to the diagram below. )

Another aspect of variables is that ownership of variables can be transferred to other variables as well. This is the part that may not be present in any other programming language. Let’s take an example by considering a real-life scenario shown below.

Let’s say User A bought a book on Amazon. This user is now the owner of the book. After a few days, this user decides to sell the book as a used one. User B now buys the book. User A no longer has access to the book. It’s shipped to User B and we know that the ownership is transferred to B.

In this example, the book is the piece of data that a variable may hold. We can represent the above scenario using the code below. We created two variables. When we assigned user_b the value of user_a, Rust moves the ownership to user B just like it would happen in real life.

Ownership of Book belongs to User A Lord of the Rings
Ownership of Book belongs to User B Lord of the Rings

Now try to access owner_a and print again to see what the compiler output is.

As logic would dictate, when we tried to access user_a’s book name when the user didn’t own it in the first place we shouldn’t be allowed to. And that’s exactly what Rust’s compiler told us with the error message below.

error[E0382]: borrow of moved value: `user_a`
--> src/main.rs:11:52
|
7 | let user_a = Book{name : "Lord of the Rings".to_string()};
| ------ move occurs because `user_a` has type `Book`, which does not implement the `Copy` trait
8 | println!("Ownership of Book belongs to User A {}" , &user_a.name);
9 | let user_b = user_a ;
| ------ value moved here
10 | println!("Ownership of Book belongs to User B {}" , user_b.name);
11 | println!("Can user A :{}: access this book ? " , user_a.name);
| ^^^^^^^^^^^ value borrowed here after move

Note that primitive types don’t have this error because the values ​​are copied when we try to assign them. In complex types and all data that is stored in Heap, this is the behavior that we expect to see in Rust.

We looked at what variables are, and how Rust manages them. Now let’s look at what Reference is all about.

A reference is a pointer to a particular data. As the name suggests, a reference points to the address of the memory location. This may sound similar to references in C++. We can define a reference by using the & keyword.

A is stored at address 0x7ffeb6aa08e4
New reference variable address 0x7ffeb6aa08e4

Notice that both these variables point to the same memory address. A reference is immutable to read-only pointer to the data in memory.

A value can be updated as well by using a mutable reference. Refer to the code below.

To mutate a value using a reference, the original value must be declared mutable by using mut keyword. Click here for more about immutability here.

It is important to understand that when we create a reference a_ref , we are borrowing accessing some data from the owner. The owner being the variable a here. We can use the data immutably or mutably, but ownership is still with the original variable. This reference has a lifetime which must always be valid.

Let’s look at another analogy to understand this. Imagine if you decide to borrow a book from the library. You can go to the library and check out a book that can be read for two weeks before it is returned. The book is still owned by the library, but one just has permission to read it. It needs to be returned after a certain period or when you finish reading it. As soon as the book is returned, it can be borrowed by someone else for their use. While you have the book, if it gets stolen or lost, there will be consequences, and you must make sure that the book is always safe. Additionally, nobody else can borrow the same book while you have it. Another user may borrow a different copy of the same book.

This is quite restrictive, but remarkably like using references in rust. References borrow the data at a location owned by a variable. And there are rules that exist when working with references. We won’t go into a lot of detail into these rules. You can read it here in another article. But it is helpful to understand that, just like variables — references can be treated as a special type of variable.

When getting started with references, RUST it may feel like we’re hitting a wall every single time. The more we think of data and memory around some simple concepts, the easier it will be working with RUST.

So, we’ve seen what variables are, what references are, and some basic ideas of the usage of references. We’ve also been talking about ownership, and the scope of variables. It’s time we look at lifetimes.

As seen in the example below, variables and their memory is dropped as soon as the scope finishes. And the compiler ensures that these checks are done perfectly.

error[E0425]: cannot find value `b` in this scope
--> src/main.rs:6:57
|
6 | println!("A value is {} . And b value is {} " , a , b);
| ^ help: a local variable with a similar name exists: `a`

Rust compiler ensures that references and variables are both always valid. Scopes are quite a straightforward way to think of this.

Since references point to the data in memory, Rust must ensure that there are no invalid references at any point in the program. We don’t want to have references when the actual data has no meaning or is completely removed. No invalid access/operation should happen on references. And the way Rust compiler ensures all these checks are via lifetimes. Lifetimes are the compiler’s way of checking for these cases and ensuring that any reference that we have is always valid. These are more advanced than scopes, but it’s natural to think in terms of scopes. This is exactly where Rust shines, and there is almost no possibility of invalid memory access, failures or vulnerabilities related to memory and data access while using safe Rust.

We won’t go into a lot of detail in this article. But we will see how rust helps us when we talk about lifetimes and references.

Considering the example above, we see that the scope of variable a is valid till the end. But notice that a is assigned as a reference to y within the inner block. As soon as the inner scope block on line 6, the value of y is dropped. Normally, we would say that a now holds a garbage value. Because the variable to which a points don’t exist. But this is an invalid operation in rust. Rust identifies that the lifetime of y is smaller than the lifetime of a. And that accessing it beyond that point is simply wrong. Hence it throws the following compile time error.

error[E0597]: `y` does not live long enough
--> src/main.rs:5:13
|
5 | a = &y;
| ^^ borrowed value does not live long enough
6 | } // y is dropped
| - `y` dropped here while still borrowed
7 |
8 | println!("The value of 'x' is {}.", a);
| - borrow later used here

It clearly states that y doesn’t live long enough, and some references may be invalid.

I found this quite hard to grasp when I started, but it feels very natural to me now. The way these things are modeled helps us with a lot of bugs that may happen at runtime. Imagine the race conditions that may come when we are working with parallel and concurrent programs. Lifetimes save us from a lot of these aspects and the compiler does most of the heavy lifting for us.

I’ll try to write in detail about lifetimes, and some ways to code them soon. Hope that this article gave a good introduction to all these concepts.

Leave a Comment