This post published on my blog before
Hi everyone. Before that, I wrote a post called Ownership Concept in Rust.
Today I'll try to explain some of the features called borrowing and references. I called them laws in the title.
Before starting, I’ll create a project with cargo;
cargo new references_and_borrowing
cd references_and_borrowing
Introduction
We've discussed the ownership concept in the last post. When you assign a heap-allocated variable to another one, we also transfer its ownership, and the first variable we created is no longer used. This explains why Rust is a secure programming language. Rust's memory management mechanism is different than others.
For example, when you passed a variable to a function as a parameter, that variable is no longer used. It looks like annoying. You don't want to always create new variables to do the same stuff.
Sometimes you don't want to move variable's ownership but you also want to its data. In these kinds of cases, you can use the borrowing mechanism. Instead of passing a variable as a value, you send a variable as a reference.
References hold memory addresses of variables. They don't hold the data of variables.
What are Benefits?
The compiler always checks the reference object statically. It doing this with a mechanism called borrow checker. So, it prevents to delete object while it has a reference.
Let's See Examples
The first example will be like that;
fn main() {
let my_name = String::from("Ali");
//this ampersand is a reference
let len_name = name_length(&my_name);
// -----------------------^
}
fn name_length(string: &String) -> usize {
// ----------------^
string.len()
}
In the above example, we've passed a variable as a reference. When you use &
ampersand symbol, it's transforming to a reference. The key point is here if we pass a reference variable to a function as a parameter, that function will accept a reference parameter. If you don't use an ampersand you'll see a compile-time error like that;
--> src/main.rs:5:32
|
5 | let len_name = name_length(&my_name);
| ^^^^^^^^
| |
| expected struct `std::string::String`, found `&std::string::String`
| help: consider removing the borrow: `my_name`
Our second example will be like that;
fn main() {
let boxed_integer = Box::new(5);
let reference_integer = &boxed_integer;
println!("Boxed: {} and Reference {}", boxed_integer, reference_integer);
}
As you see, when we send or assign a parameter, our assigned or passed values starts with an ampersand. What would happens if we didn't use this ampersand? It won't work. Because when we assigned boxed_integer
to reference_integer
, compiler removed the boxed_integer
variable.
A Problem About Removed Resource
Let's think of a scenario. We know that when references are available, the resource won't be removed. What will happen if the resource removed before the reference? Let me say the reference will point to a null unit or random unit. This is a really big problem here. But don't worry about it. The Rust avoids this problem thanks to the compiler. Let's see an example.
fn a_function(box_variable: Box<u32>) {
println!("Box Variable {}", box_variable);
}
fn main() {
let box_variable = Box::new(6);
let reference_box_var = box_variable;
a_function(box_variable);
}
We'll see an error here;
error[E0382]: use of moved value: `box_variable`
--> src/main.rs:9:16
|
6 | let box_variable = Box::new(6);
| ------------ move occurs because `box_variable` has type `std::boxed::Box<u32>`, which does not implement the `Copy` trait
7 | let reference_box_var = box_variable;
| ------------ value moved here
8 |
9 | a_function(box_variable);
| ^^^^^^^^^^^^ value used here after move
error: aborting due to previous error; 1 warning emitted
To solve this problem, we could create a new scope;
fn a_function(box_variable: Box<u32>) {
println!("Box Variable {}", box_variable);
}
fn main() {
let box_variable = Box::new(6);
// a scope
{
let reference_box_var = &box_variable;
}
a_function(box_variable);
}
Mutable Borrow
Until now, we always see immutable borrow examples. But sometimes you will need mutable borrows. We only read values from immutable borrow resources. We didn't change reference's value.
Normally when we accept reference we use this notation &T
but if we want to mutable reference we should use &mut T
. However, we can't create a mutable reference while the variable is immutable. Let's see an example
fn main() {
let mut a_number = 3;
{
let a_scoped_variable = &mut a_number;
*a_scoped_variable +=1;
}
println!("A number is {}", a_number); // A number is 4
}
let a_scoped_variable = &mut a_number;
*a_scoped_variable +=1;
In the above example, if we didn't use mut
the following line won't work. And if we didn't create a variable with the mut
keyword at the beginning, the compiler will throw an error. Because we were trying to get a mutable reference from an immutable variable.
There is also a key point we should know that you can have only one mutable reference. So you can't create multiple mutable references. I give an example that will fail.
fn main() {
let mut number = Box::new(5);
let number2 = &mut number;
let number3 = &mut number;
println!("{}, {}", number2, number3);
}
It will show an error like that;
Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `number` as mutable more than once at a time
--> src/main.rs:5:19
|
4 | let number2 = &mut number;
| ----------- first mutable borrow occurs here
5 | let number3 = &mut number;
| ^^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", number2, number3);
| ------- first borrow later used here
The benefit of having this restriction is that Rust can prevent data races at compile time. A data race is similar to a race condition and happens when these three behaviors occur:
1-) Two or more pointers access the same data at the same time.
2-) At least one of the pointers is being used to write to the data.
3-) There’s no mechanism being used to synchronize access to the data.
We can fix this error by creating a new scope;
fn main() {
let mut number = Box::new(5);
let number2 = &mut number;
{
let number3 = &mut number;
}
}
There is a similar rule like this rule when you're combining mutable and immutable references.
fn main() {
let mut number = Box::new(5);
let number2 = &number;
let number3 = &mut number;
println!("Number2 {} and Number3 {}", number2, number3);
}
This code piece won't work. You'll get an error;
Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `number` as mutable because it is also borrowed as immutable
--> src/main.rs:5:19
|
4 | let number2 = &number;
| ------- immutable borrow occurs here
5 | let number3 = &mut number;
| ^^^^^^^^^^^ mutable borrow occurs here
6 |
7 | println!("Number2 {} and Number3 {}", number2, number3);
| ------- immutable borrow later used here
We can fix this code by this way;
fn main() {
let mut number = Box::new(5);
let number2 = &number;
println!("Number2 {}", number2);
let number3 = &mut number;
println!("Number3 {}", number3);
}
It worked. Because a reference’s scope starts from where it is introduced and continues through the last time that reference is used.
Dangling References
In languages which have pointers, you can create dangling pointers easily. For example, this problem occurs in C programming language like that;
int *afunc();
void main()
{
int *pointer;
pointer = afunc();
fflush(stdin);
printf("%d", *pointer);
}
int * afunc()
{
int x = 1000;
++x;
return &x;
}
It should throw an output like that;
warning: function returns the address of the local variable
So, what's happening in Rust programming language? The compiler guarantees that references will never be dangling references. For example, you wrote code like the above code, the compiler, won't compile your code. It will ensure that the data will not go out of scope before the reference to the data does.
Let's try to create a dangling reference and we'll sure Rust will prevent to compile your code.
fn main() {
let dangling_reference = a_func();
}
fn a_func() -> &Box<u8> {
let x = Box::new(10);
&x
}
We'll see an error at compile-time;
Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn a_func() -> &Box<u8> {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
5 | fn a_func() -> &'static Box<u8> {
| ^^^^^^^^
As you see, our code didn't compile. The solution is to return your data directly. Don't use ampersand.
fn main() {
let dangling_reference = a_func();
}
fn a_func() -> Box<u8> {
let x = Box::new(10);
x
}
What Did We Learn?
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
That’s all for now. If there is something wrong, please let me know.
Thanks for reading.
Top comments (1)
hey, I am just starting to learn rust and this was very helpful.
Borrowing and Ownership are the main features of rust I would say. Which makes Rust quite different from other languages.