Rust Ownership 4: References and Borrowing

ยท

10 min read

In part 3 of this series, we learned what Ownership in Rust means and some of the ways ownership of a value can be transferred (moved). We also learned about the Borrow-Checker and how it prevents us from illegally accessing data in Rust.

We also came to the conclusion that it would be tedious if we could never use a variable again after passing it into a function (because ownership has been moved), and I said that Rust allows us to borrow values rather than take up their ownership.

In this fourth and final part of this series, we will go through what borrowing in Rust means and how references help us do that. Let's go ๐Ÿš€

References in Rust

Recall the following program introduced in the last article:

fn main() {
    let s1 = String::from("lisabi");
    let s2 = s1;
    println!("s1: {}, s2: {}", s1, s2);
}

[ You can play with this code here ]

Recall that the program fails to compile because, as we learned, when s2 = s1 was executed, ownership of the value that s1 pointed to was moved into s2, and then s1 became invalid. You can run the code on the Rust playground to see this in action.

Now, I want to do something I haven't explained but will make the program compile and run, do not worry, I will explain afterwards (trust me ๐Ÿ˜‰)

fn main() {
    let s1 = String::from("lisabi");
    let s2 = &s1;
    println!("s1: {}, s2: {}", s1, *s2);
}

[You can execute this code here]

The change is subtle; you might miss it. Can you spot the difference?

By changing s2 = s1 to s2 = &s1 we got the program to compile, and this time the borrow-checker did not complain about us trying to access a value that had been moved, like in the previous program.

Let me explain what just happened.

In Rust, when will prepend a variable name with & as in &s1, we are creating what is called a reference to the value the variable holds. When you create a reference to a variable in Rust, you are not taking ownership of it; instead, you are borrowing it. Think of a reference like a memory address, and Rust can follow that address to access the data stored at the address.

s2 now holds a reference to the value that s1 points to (instead of taking ownership away from s1). Also notice that the line that prints out s2 accessed it by using *s2 : this is known as dereferencing. Deferencing is how we access the data stored at a particular address. Although, when we are printing s2 we don't necessarily need to dereference it as I did because Rust will do that for us automatically. This will work too:

fn main() {
    let s1 = String::from("lisabi");
    let s2 = &s1;
    println!("s1: {}, s2: {}", s1, s2); // s2 printed without manually dereferencing
}

The Gist?

The gist of the discussion is that whenever we put a & before a variable in Rust, it is as if we are telling Rust "I know about your ownership stuff and that I can't use a variable after moving ownership of its value to another variable...but can I borrow the value instead๐Ÿ™? "

Functions and Borrowing

Recall that we learnt that Ownership can also be transferred into and out of functions, we can also 'tell' a function to borrow a value instead of taking ownership of it. Let's look at an example:

// notice that the parameter s is typed as &String instead of String
// This means that calculate_length will borrow the value of the variable 
// being passed into it rather than take ownership of the value
fn calculate_length(s: &String) -> usize {
    s.len()
}

fn main() {
    let s1 = String::from("lisabi");

    // notice that we pass s1 into the function as &s1
    // meaning we are borrowing its value
    let length = calculate_length(&s1);
    // we can still access s1 after another function borrows its value
    println!("The length of '{}' is {}.", s1, length);
}

[You can execute this code here]

Notice that the parameter in calculate_length(s: &String) is typed as &String instead of String; this is how we indicate that the function is not going to take ownership of the value of the variable passed into it; it is going to borrow it.

Also, when we call calculate_length(&s1) inside the main function, we pass the argument in as &s1, this indicates that we the function is borrowing the value of the s1 variable instead of taking ownership of it.

How does this work?

Recall that we learned that when a variable goes out of scope, Rust drops it, and it becomes invalid to access it. You might ask that when calculate_length goes out of scope, why wasn't s1 dropped?

The reason s1 is not dropped when calculate_length finishes execution is that calculate_length doesn't own it. It is a reference to s1 that we passed into the function (like this calculate_length(&s1) ), so when the function goes out of scope, it drops the reference to s1 (not s1 itself). And that is why the variable s1 is still usable after the call to calculate_length(&s1), neat right ๐Ÿ˜‰?

Another question you might ask is: what is in s , the variable inside of calculate_len

Recall from the anatomy of a string in the previous article where we said a string has some string data stored in the stack and the actual value of the string is on the heap? If you haven't read it, please do so here. s inside of calculate_length holds a reference to s1 and it can be visualized as this:

We can see that the string data ptr of s points to the pointer of s1, this is because s holds a reference to s1.

Mutable References

Just as variables are immutable by default, whenever we reference a variable, we are not allowed to modify it by default. But there are times when you need to modify something you are borrowing (e.g., a list of elements); in that case, you can define a mutable reference.

We define a mutable reference by adding mut after the & when we want to borrow. Let's look at an example:

fn mutate(s: &mut String) {
    s.push_str(" egba");
}

fn main() {
    let mut s1 = String::from("lisabi");

    mutate(&mut s1);
    println!("s1 after mutation is {}.", s1);
}

[You can execute this code here]

First, we had to declare s1 as mutable by declaring it as mut s1 , otherwise we won't be able to change it. Notice that the mutate function declared expects its input to be of the type &mut String . Recall that & indicates that we are borrowing the value and mut is to enable us to change the value while we are borrowing it.

Is it possible to have multiple mutable references?

Imagine what it would look like if there were multiple mutable references to the same data. Let's try to understand what this will look like with an analogy. Imagine two friends working on the same document in a collaborative editor:

  • The two friends (mutable references) attempt to edit the same document (data) simultaneously.

  • They both start making changes, potentially overwriting each other's edits.

  • This will lead to a data conflict.

  • To prevent this, we could enforce a rule that only one friend can edit the document at a time - but multiple people can view it at the same time.

To avoid data conflicts like the one illustrated, Rust also has restrictions around mutable references: Once you have an active mutable reference to a value, you cannot make other references to that value.

For example, the following program will not compile at all:

fn main() {
    let mut s = String::from("lisabi");

    let ref1 = &mut s; // <--- mutable reference to s
    // more code
    //....
    let ref2 = &mut s; // <--- an attempt to have another reference to s

    println!("{}, {}", ref1, ref2);
}

[You can execute this code here]

In the code example, ref1 borrows s as a mutable reference and then later on in the code, ref2 tries to create another reference to s. This has the potential to create a data conflict, so Rust doesn't allow it.

However, let me show you something interesting. Let's modify the program a little by only printing out ref2:

fn main() {
    let mut s = String::from("lisabi");

    let ref1 = &mut s;
    // more code
    //....
    let ref2 = &mut s;

    println!("{}", ref2); // <--- we are no longer printing out ref1 here
}

This compiles and prints out the value of ref2. What!! Is this not a contradiction?

It is not; trust me. Now let me convince you why it is not.

The Rust compiler is pretty smart; it can tell that ref1 was never used again after the second reference was created, which means there is no chance for ref1 to be modified while ref2 still points to s - i.e no chance for a data conflict, so the Rust compiler allows it.

This can also be noticed if a mutable reference goes out of scope; we can then create another reference without problems. See this example:

    ...
    let mut s = String::from("lisabi");
    {
        let ref1 = &mut s;
    } // ref1 goes out of scope here, so we can have a new reference to s without problems

    let ref2 = &mut s;
    ...

In this case ref1 is defined within {..} and it is restricted to that scope, so after ref1 has gone out of scope, we can create another reference to s without problems.

I have a question for you: will the following program compile? And why or why not?

fn main() {
    let mut s = String::from("lisabi");
    let ref2 = &mut s;
    {
        let ref1 = &mut s; // is this valid?
    } 
    println!("{}", ref2);
}

I hope you gave it some thought. It won't compile because even though ref1 was defined within an inner scope and went out of scope afterwards, there was still an active mutable reference ref2 to s when ref1 was being created.

What about mutable and immutable references at the same time?

Just to be clear, you can have as many immutable references to a variable as you want, i.e., borrow it without modifying it as much as you want. But you can't have more than one mutable reference to a variable.

You also cannot have a mutable reference while you have an immutable reference to the same value.

This makes sense; if you have an immutable reference to a value, that means that you have a guarantee that the value will not change while you are still holding on to it, so there should be no mutable references allowed to it.

let mut s = String::from("lisabi");

let ref1 = &s; // this is fine
let ref2 = &s; // this is fine too
let ref3 = &mut s; // nah, this is not fine!!!

println!("{},{}, and {}", ref1, ref2, ref3);

The program above will not compile.

I have one last question for you: will the following program compile?

fn main() {
    let mut s = String::from("lisabi");

    let ref1 = &s; // this is fine
    let ref2 = &s; // this is fine too
    println!("{}, {}", ref1, ref2);

    let ref3 = &mut s; // IS THIS FINE?
    println!("{}", ref3)
}

I hope you also gave it some thought. And the answer is...yes it will compile.

We saw a similar example earlier. When ref3 , a mutable reference to s is being created, the other references ref1 and ref2 were no more used after then - it is like their scopes have ended and ref3 is the only reference left to s.

Conclusion

Wow๐Ÿคฉ

This has been a very long ride from Part 1 through Part 4. Thank you for joining in on the journey. I hope you enjoyed reading it as much as I enjoyed writing it. Making the illustrations was very fun ๐Ÿ‘Œ

In this final article, we learned about what references are in Rust and how they enable us to borrow access to values. We also learned about mutable and immutable borrows and the rules guiding them.

That concludes the end of this series. If you enjoyed this, consider joining my newsletter and also checking out my YouTube channel.

Happy Coding

References