You probably know that this code does not compile
fn main() {
let my_vec = vec![1, 2, 3];
let my_other_vec = my_vec;
println!("{:?}", my_vec); // error!
}
but this code does:
fn main() {
let my_number = 1;
let my_other_number = my_number;
println!("{:?}", my_number);
}
It happens due to the key principle of Rust language: the ownership system. Two main things happen here:
Vec<T>
does not implement theCopy
trait, so its value will be moved tomy_other_vec
and we won't be able to usemy_vec
after that.i32
does implement theCopy
trait, so its value will be copied tomy_other_number
, and we can still use themy_number
variable.
However, in the real world, we often deal with types that don't implement the Copy trait, and having to move things around multiple times would become a real headache if there was no alternative. But Rust has a way to use values without moving them: borrowing.
fn main() {
let my_vec = vec![1, 2, 3];
add_one(&my_vec)
}
fn add_one(v: &Vec<i32>) {
println!("{:?}", v);
}
If the add_one
function were to accept a Vec instead of a &Vec, any vec passed to this function would be moved into it, rendering the original value unusable. Since this is not the behavior we want, our function now accepts a reference to a value, instead of the value itself.
Immutable references (&T
) - reading a value
The previous snippet shows a scenario in which we need to borrow a value only for reading, without modifying it. Just like variables in Rust, which are immutable unless you put the keyword mut
in the declaration, references are also immutable by default.
This behavior guarantees that we do not accidentally change the value, since we are only reading from it. For this reason, we can have as many immutable references as we want - and that's the reason why the type &T
is also called a shared reference.
However, the use of immutable references follows some additional rules.
Let's imagine a scenario in which we need to create a Vec<i32>
based on another Vec<i32>
, but we only have access to the reference pointing to the Vec
.
let my_vec_ref: Vec<i32> = &my_vec;
There is a type mismatch here, since my_vec_ref
is supposed to be a Vec
, not a reference to one. However, there is a solution: Rust provides the "inverse" operator of &
: the *
operator, which lets us access the value behind a reference!
fn main() {
let my_vec: Vec<i32> = vec![1, 2, 3];
let my_vec_ref = &my_vec;
let another_vec = *my_vec_ref; // oops!
}
... but not to move it.
The code above gives us the following error:
cannot move out of `*my_vec_ref` which is behind a shared reference
move occurs because `*my_vec_ref` has type `Vec<i32>`, which does not implement the `Copy` trait
Alright, we know that Vec<i32>
does not implement the Copy
trait and stuff, but what is this error actually telling us?
Moving out
The =
operator creates an assignment expression. According to Rust docs, an assignment expression moves a value into a specified place.
Things are pretty straightforward when we are dealing with straightforward types, but a reference is what we call an indirection type - a type that has a "layer" between you and the real data. In this case, we can imagine the reference as a wrapper for the data. Due to its immutability, we are not allowed to either change what's inside of it or move what's inside of it. In other words, we can't "move out".
It also implies that we can only move things from values we own, never from values we borrow - which is another way to say the same thing.
Additionally, in Rust the .
operator automatically dereferences reference types, causing the same error to happen when we try to do things such as the code below.
struct MyStruct {
str_vec: Vec<i32>
}
fn main() {
let strct = MyStruct { str_vec: vec![1, 2, 3]};
let strct_ref = &strct;
let my_vec = strct_ref.str_vec; // error!
}
This behavior is useful when talking about methods that take self
by reference, ommiting the use of *
operator:
struct MyStruct { n: u32 };
impl MyStruct {
// no need to type (*self).n
fn method(&self) -> u32 { self.n }
}
let x = MyStruct { n: 12 };
// Here the compiler automatically references self,
// so that there's no need to write (&x).method()
let n = x.method();
The solution
Back to one of our previous examples:
fn main() {
let my_vec: Vec<i32> = vec![1, 2, 3];
let my_vec_ref = &my_vec;
let another_vec: Vec<i32> = *my_vec_ref; // oops!
}
In cases like this, the error messages may contain some misleading instructions, such as the following:
consider borrowing here: `&*my_vec_ref
This is not possible here, since it means assigning a &Vec<i32>
to a value that has type Vec<i32>
.
Therefore, the solution when this error happens is to clone the value - so that you get a copy from it without moving it.
let another_vec: Vec<i32> = my_vec_ref.clone();
Mutable references (&mut T
)
Modifying a non-copy value follows the logic of moving or borrowing. You can either move the value in order to modify it, or use a mutable reference (borrow) to do so.
Let's create a function that modifies a value using a &mut T
:
fn main() {
let mut my_vec = vec![1, 2, 3];
let ref_my_vec = &mut my_vec;
add_one(ref_my_vec);
println!("{:?}", my_vec);
}
fn add_one(v: &mut Vec<i32>){
v.push(1);
}
When we assign a mutable reference to ref_my_vec
, it means that this variable points to an existing value, instead of owning a new value. Therefore, modifying ref_my_vec
is equivalent to directly modify my_vec
. We can say that a mutable reference provides us with write access to a value.
However, since the only way to securely modify a value is to have an unique write access to it (think about data races), Rust does not allow us to have more than one mutable reference to the same value simultaneously.
let ref_my_vec = &mut my_vec;
let another_ref = &mut my_vec;
// cannot borrow `my_vec` as mutable more than once at a time
This is the reason why a mutable reference is also called an unique reference.
Similarly to shared references, Rust also does not allow us to move out of mutable references. In fact, moving out from any kind of reference - mutable or not - is not allowed.
fn main() {
let mut my_vec: Vec<i32> = vec![1, 2, 3];
let my_vec_ref = &mut my_vec;
let another_vec = *my_vec_ref; // oops!
}
cannot move out of `*my_vec_ref` which is behind a mutable reference
move occurs because `*my_vec_ref` has type `Vec<i32>`, which does not implement the `Copy` trait
Mutability with references
What is the difference between p: &T
, mut p: &T
, p: &mut T
and mut p: &mut T
?
When talking about a variable whose type is a reference, it is important to differentiate what does it mean to change what the reference points to and what our variable points to.
Therefore, you can do the following:
let vec_number = vec![1, 2, 3];
let another_vec = vec![2, 3, 4];
let mut ref_vec = &vec_number;
ref_vec = &another_vec;
Let's see what is happening here.
Although we cannot change the data the ref_vec
variable points to, we can make ref_vec
point to another data (as long as it has the same type of the data previously assigned).
Therefore, when we put the mut
keyword in the left side of an assignment expression, we are telling Rust that we are going to reassign that variable to another value.
Type | Can reassign the variable | Can change the data the variable points to |
---|---|---|
p: &T | ❌ | ❌ |
mut p: &T | ✅ | ❌ |
p: &mut T | ❌ | ✅ |
mut p: &mut T | ✅ | ✅ |
Top comments (2)
This is by far the best explanation of Rust references I have ever seen, simple and easy to grasp the concepts -- even though they are tricky and can be a source of great pain... Great work!
Best