DEV Community

Cover image for Beginner's guide to Rust references
wrongbyte
wrongbyte

Posted on • Edited on

Beginner's guide to Rust references

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!
}
Enter fullscreen mode Exit fullscreen mode

but this code does:

fn main() {
    let my_number = 1;
    let my_other_number = my_number;
    println!("{:?}", my_number);
}
Enter fullscreen mode Exit fullscreen mode

It happens due to the key principle of Rust language: the ownership system. Two main things happen here:

  • Vec<T> does not implement the Copy trait, so its value will be moved to my_other_vec and we won't be able to use my_vec after that.

  • i32 does implement the Copy trait, so its value will be copied to my_other_number, and we can still use the my_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);
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

... 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
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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!
}

Enter fullscreen mode Exit fullscreen mode

In cases like this, the error messages may contain some misleading instructions, such as the following:

consider borrowing here: `&*my_vec_ref
Enter fullscreen mode Exit fullscreen mode

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(); 
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
dbschwartz profile image
Cory Isaacson

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!

Collapse
 
shafi_munshi profile image
Shafi Munshi

Best