Learn Rust with Me: Rust Borrow Checker
December 6, 2022
When those of us who are used to writing code in languages like JavaScript and Python, it can be difficult to wrap our heads around the borrowing feature that Rust uses to increase memory efficiency. While it may seem like just another programming concept you just have to learn, it is actually a very important feature of Rust that allows programmers to create fast and memory-safe applications. Let’s explore a little bit about borrowing in Rust.
What is borrowing?
Let’s start with the most basic programming concept: the variable.
fn main() {
let x = String::from("test");
}
Here we have added a variable called x
to memory that has the value of "test"
. Not too complicated, right? Right now, the main
function has ownership of the x
variable. Now, let’s use our variable in a function:
fn add_one(string:String) {
println!("x + 1 = {}1", string);
}
fn main() {
let x = String::from("test");
add_one(x);
}
Something very important to note here: When we call the function add_one
in this example, the x value is passed into the function. That’s it. It’s gone. It can no longer be used after this point. For example, if we tried to do this:
fn add_one(string:String) {
println!("x + 1 = {}1", string);
}
fn main() {
let x = String::from("test");
add_one(x);
println!("{}", x); //This won't work!
}
We will get an error saying borrow of moved value: "x"
. This happens because x
is essentially no longer there. When we pass x
to add_one
, it remains in memory until add_one
is completed or it gets passed somewhere else.
So how do we use x
in our function without losing ownership of the variable? That’s where borrowing comes in.
References
In Rust, you can pass a reference to a variable in memory without actually changing its ownership. This is great! This makes it harder to accidentally leak data in applications. But how do we do it?
To pass a reference to a variable, we need to add an &
in front of the variable name:
fn add_one(string:String) {
println!("x + 1 = {}1", string);
}
fn main() {
let x = String::from("test");
add_one(&x); //This won't work yet
println!("{}", x);
}
Great! Now the function add_one
is borrowing the value of x
without actually taking ownership of it. However, we need to change one more thing to make this compile. If we run this function now, we will get the following message:
add_one(&x);
| ------- ^^ expected struct `String`, found `&String`
| |
| arguments to this function are incorrect
This is because when we defined our function, we said that the string
variable would be of type String
. We need to change this so that the function will expect a reference instead. Luckily, all we have to do is add &
in front of the parameter in the function definition:
fn add_one(string:&String) {
println!("x + 1 = {}1", string);
}
fn main() {
let x = String::from("test");
add_one(&x);
println!("{}", x);
}
Great! Now, when we run this function, we will see the following:
x + 1 = test1
test
Conclusion: Focusing on Memory Safety
Having this capability built-in to the compiler is one of the many reasons that programmers are shifting their focus to Rust. Other programming languages, such as JavaScript and Python, use garbage collection to determine when to remove an item from memory. This makes work easier for the programmer, but it comes with a high performance cost. It also makes it easier for you, the programmer, to not pay attention to how memory is being stored in your application.
This is one of the many features that I love about Rust. The Rust compiler forces you to focus on these important parts of our applications, making the end result a much more memory-safe product.