Rust is a statically typed programming language that allows developers to build high-quality and efficient software.
Rust takes memory management very seriously. While it allows you to manage your memory, it does so in a way that provides safety guarantees. Rust’s memory management system is based on the concept of ownership and borrowing. The ownership and borrowing system is a unique feature of Rust that brings together a combination of ideas that provides memory safety without sacrificing performance.
For example, in Rust, two variables can not have ownership of one resource at a time. Take a cue from the code and the comments below:
fn main() {
let x = String::from("Hello World"); // x is a String variable
let y = x; // y now owns the value of x
println!("x = {}", x); // this line would cause a compile-time error because x is no longer valid, it has been moved.
println!("y = {}", y); // prints "y = Hello World"
} // y goes out of scope here and Rust frees the memory allocated for the String value
As you can see in the piece of code above, x
was assigned to y
, therefore, the ownership and memory allocation of x
was transferred to y
. So, x
no longer exist.
Interestingly, this behavior does not exist in integer or floating point number types as they implicitly implement the Copy
trait.
What that means is that most number types in Rust implement a trait that allows them to create a new memory allocation each time they are re-assigned to a variable. See the example below:
fn main(){
let a = 5.999;
let b = a; // a copy happens here, instead of a move
println!(" a: {}, b: {}", a, b) // prints "a: 5.999, b: 5.999"
}
Another way Rust allows you to efficiently use variables is by referencing them. So, instead of copying or cloning the variable which might come with a cost, you could just reference it.
Technically, referencing is the act of creating a reference to a value, which allows a program to access the value without taking ownership of it. References are created using the &
operator. See the code snippet for more context
fn main(){
let a = String::from("Hello World!");
let b = &a; // b is holds a reference to a
println!(" a: {}, b: {}", a, b) // prints " a: Hello World!, b: Hello World!"
}
There are two types of references in Rust: shared references and mutable references. Shared references are created using &
. They allow multiple parts of a program to read the value but not modify it. However, mutable references which are created using &mut
allows a part of a program to modify the value.
Here is an example of a mutable reference and how you can use it to change the main value.
fn main() {
let mut x = 5;
let mut_ref = &mut x; // create a mutable reference to x
*mut_ref = 10; // change the value of x through the mutable reference
println!("x = {}", x); // prints "x = 10"
}
Notice the *
before the mut_ref
?
That is another concept you should know about, now that you are familiar with referencing in Rust. It’s known as the dereferencing operator. It allows you to access the value the reference is pointing to and change it.
As you can see in the piece of code above, we’ve been able to change the value of x
by changing the value of mut_ref
.
Now that we have laid the background for understanding referencing, let’s take a quick look at lifetimes in Rust. Lifetimes are a way to ensure that references remain valid for as long as they are being used or needed. Lifetimes are designed to solve dangling pointers or use-after-free bugs. A dangling pointer is a pointer that points to a memory location that has already been freed or deleted.
Here is a simple example of lifetimes
fn main() {
let num1 = 3;
let num2;
{
let x = 10;
num2 = &x;
} // the lifetime of a reference to `x` ends here
let sum = num1 + num2;
println!("{}, Here", sum);
}
The code snippet above defines two variables, num1
and num2
, within the main
function. However, an inner scope is created within main
where an attempt is made to borrow a reference to x
, which was defined within that inner scope. As the reference’s lifetime is limited to that inner scope, it cannot be borrowed outside of it. This means that the reference can only be used within the scope in which it was created, and it cannot be accessed from outside that scope.
While lifetimes exist naturally in Rust, most times you’ll see explicit lifetime annotations being used like so:
fn add<'a>(x: &'a i32, y: &'a i32) -> i32 {
x + y
}
The code snippet above defines the lifetime annotation ‘a (which can be replaced by any letter or word) and applies it to the function parameters. This indicates that the lifetime of both ‘x’ and ‘y’ must be identical wherever they are utilized; otherwise, an error will occur.
Lifetimes are more complex than this — depending on the context they are being used. This is just scratching the surface, you should check the docs for more.
There is a whole lot to learn about Rust, if this interests you and you are looking to start your journey with Rust, I suggest you start with the Rust Book, it’s free. Follow me on Twitter @ezesundayeze or connect on LinkedIn
Happy Hacking!