Open-Source Internship opportunity by OpenGenus for programmers. Apply now.
One of the key elements Rust has, which is also considered it's most unique feature, is the Ownership model. This allows memory safe operations without a garbage collector, and completely avoids (if using safe rust), some of the big problems other languages run into when it comes to memory management.
Table of contents:
- Ownership
- References and Borrowing
- Lifetimes
Let us get started with Ownership, Borrowing and Lifetimes in Rust.
What is Ownership?
Ownership is pretty easy to explain conceptually, although it's effects on the way of programming and approaching problems are a bit more complex.
Let's suppose you buy something, let's say, a mouse. You now own that mouse. It is yours to use and keep. Simple enough right? Now, let's move to Rust terms. Ownership has three main rules.
- Each value in Rust has a variable that's called it's owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (Freed, in terms of memory)
Let's start with the last one. The scope.
fn main() {
let number = 5;
if number > 0 {
let other_number = 7;
}
println!("Number is: {}, and other_number is {}.", number, other_number);
}
This does not compile.
Why? Our curly brackets define different scopes. As you can see, our variable "other_number" is within an if statement with it's own curly braces. Once the if ends, other_number is out of scope, which makes it drop. So we cannot use it in our print statement!
Simple enough right? Let's move on to some other more complex data types, to show some not-so-straightforward behaviour. (Following images taken from the Rust Book, link below as always!)
fn main() {
let first_string = String::from("Opengenus");
let second_string = first_string;
}
Let's examine closely what is happening here.
The string type is heap allocated. On the stack, we only save a few interesting tidbits of information we need, namely, a pointer to where our data begins on the heap, the capacity and length of our string. Nothing else.
So, when we say second_string = first_string, what is actually happening? Does Rust create a new pointer and copy our old data into the new one? Do we have 2 variables each with their own data, like this?
No. It does not. Copying heap data can be potentially very taxing if the data is too big. What Rust does instead is create a new pointer, that points to the same location. Like this:
But this presents an interesting problem. We have 2 variables, pointing at the same data on the heap.. and when these variables go out of scope, Rust will try to free this data, causing a double-free error. How does Rust solve it? Since they're both pointing at the same place, Rust considers our first_string to be invalid after second_string is assigned.
fn main() {
let first_string = String::from("Opengenus");
let second_string = first_string;
println!("Hello from {}!", first_string);
}
first_string was moved into second_string. But what if we DO want a copy? What if we want our two variables to each have their own data? That's where the Clone trait comes into place.
fn main() {
let first_string = String::from("Opengenus");
let second_string = first_string.clone();
println!(
"Hello from {}! And Also From {}",
first_string, second_string
);
}
In the above snippet, second string clones the first one. Cloning involves copying everything into a new variable (As seen in the second image in the listing above)
Let's go back for a moment to our numbers example.
fn main() {
let first_number = 7;
let second_number = first_number;
println!(
"First num is: {} and second is {}",
first_number, second_number
);
}
This code throws no errors. And it may seem like a contradiction. Why can't I do the same with strings? Wny isn't first number moved into second number? Basic data types, like integers in this case, are cheap to copy. Their size is known at compile time so the copy is very inexpensive. Which cannot be said for the strings, since they are heap allocated, and there's no way of knowing how big or how small it'll be.
References and Borrowing
So, what happens if we want to make a function that edits, for example, a string? Something like this:
fn main() {
let string_one = String::from("Hello! ");
string_edit(string_one);
println!("{} Nice to meet you.", string_one);
}
pub fn string_edit(mut s: String) -> String {
s.push_str(" This is Rust!");
s
}
This doesn't compile. Why? Because string_one is moved into string_edit. We just want to reference it, not own it. So here's where References and Borrowing come in. In rust, we call using a reference borrowing. The Reference operator '&' is also used for borrowing. What we need to do is borrow our string in our function. Like so.
fn main() {
let string_one = String::from("Hello! ");
string_edit(&string_one); // We borrow here
println!("{} Nice to meet you.", string_one);
}
pub fn string_edit(mut s: &String) { // We specify we are taking in a Reference to a string
s.push_str(" This is Rust!");
}
When you reference something, you cannot edit it. Because you don't own the data. You're just borrowing it for use for a bit. Now, what if we want to do that? Such is the case of that example up there. Well, Mutable Borrows exist. Instead of just using '&', you write '&mut'. We also have to state our original variable as mutable aswell. It would make no sense for a function to take in a mutable reference to an immutable variable!
fn main() {
let mut string_one = String::from("Hello! ");
string_edit(&mut string_one); // We borrow here
println!("{} Nice to meet you.", string_one);
}
pub fn string_edit(s: &mut String) {
s.push_str(" This is Rust!");
}
Lifetimes
We've covered References, borrowing and how ownership works. But there's a little detail we haven't really discussed. Rust can infer many things. In most cases, it can infer the scope of variables and their type. But if a variable can have multiple types, we as developers need to specify it. Of course this also applies, by extension, to scopes. This is a little trickier than just adding ': Type'.
I will be giving a quick summary of this, but I encourage you to head to the Rust book's relevant chapter and revise it as necessary. I'll present a similar, slightly different example than the one provided in the book
Let's suppose we want to take in 2 string slices, and return which one is shortest. Speaking of Slices...
Slices
Slices are a special data type that do not take ownership. It's a reference to a contiguous set of elements, like a vector, or a string! In the case of a string slice, it's written as &str
Back to business
Our code could look something like this..
fn main() {
let first_string = "I am the longer string!";
let second_string = "I am shorter.";
let result = shortest_of(first_string, second_string);
println!("Shortest string is: {}", result);
}
pub fn shortest_of(x: &str, y: &str) -> &str {
if x.len() > y.len() {
y
} else {
x
}
}
Uh oh. Problems. Let's have a look.
We're returning a borrowed value but.. what are we borrowing from? How can we know if that value we're borrowing does not drop before we're done? We, as developers don't know either, because it depends on our if statement. If true, it returns Y, if false returns X.. How do we solve this? Adding a lifetime parameter, although it has a weird syntax. Lifetime is annotated right after the reference operator, followed by an apostrophe ('), and then the lifetime value. Typically it's a, b, etc. For example: &'a
(Keep in mind adding a lifetime parameter does NOT change the lifetime of the variables. It only tells the compiler -hey, this will live this much.)
So, adding lifetime parameters to our function, it will look like this:
pub fn shortest_of<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
y
} else {
x
}
}
We're saying hey, this function will take for a 'a lifetime, 2 variables of 'a lifetime, and will return a slice with 'a lifetime.
So.. does it work?
Yes it does!
Well, that's it for today's article. We've briefly covered ownership, borrowing, slices and lifetimes. For more detailed information, I left the references below!
References
The Rust Book - Ownership Chapter
The Rust Book - Lifetimes Chapter
The Rust Book - Slices as Parameters