Rust and References

September 29, 2020

I’ve previously written about ownership in Rust. If you haven’t read that post I recommend doing so first as the concept is an important pre-requisite for talking about references.

One of the first lines of code I saw in Rust looked like this.

let scope = &mut v8::HandleScope::new(isolate);

There are a few concepts hidden in there, but the one that jumped out at me was &mut, what does that mean?

As I wrote about in my mut post, mut is an indicator to the compiler that the value will change.

The addition of the & means this is a value we don’t own that we’ll be changing. But that’s an oversimplification, so let’s dive in.

Passing ownership

Let’s start with the following example. We intialize a String and calculate its byte length.

Note that Strings are represented differently in Rust than you might expect.

fn main() {
let name = String::from("laurie");
let num_bytes = length(name);
println!("{}", num_bytes);
}
fn length(name_arg: String) -> usize {
name_arg.len()
}

This is a valid Rust program. But a small change breaks the whole thing.

fn main() {
let name = String::from("laurie");
let num_bytes = length(name);
println!("{}", num_bytes);
println!("{}", name);
}
fn length(name_arg: String) -> usize {
name_arg.len()
}

This is the same program as before. The only difference is the addition of println!("{}", name);. However, that single line results in the following error.

borrow of moved value: `name`

We get this error because we’ve broken the rules of ownership.

A variable can only have one owner, so ownership of name is transferred to name_arg in the length function. When this happens, it’s considered a move and name is no longer valid.

So how do we fix this program? We use a reference.

Referencing by itself

Let’s look at our previous example modified to use a reference to name.

fn main() {
let name = String::from("laurie");
let num_bytes = length(&name);
println!("{}", num_bytes);
println!("{}", name);
}
fn length(name_arg: &String) -> usize {
name_arg.len()
}

In our main function we initialize a variable, name. We pass name to the length function. However, we explicitly pass a reference to it rather than the variable itself as indicated by &.

Within length we assign the reference to the variable name_arg. So what does the body of the length function resolve to? name_arg.len() returns the len of name’s value, since that is name_arg’s reference. Then we can print out that result in the main function.

Our &mut example

If we look at our name example we recognize that the length function isn’t changing anything about name_arg. What would happen if it tried to?

The Rust compiler would error. We can’t alter an immutable value, which is what name is. So how do we fix that?

The first thing to do is tell Rust we want to mutate name by adding the mut keyword. Then we can pass a reference to name.

fn main() {
let mut name = String::from("laurie");
length(&name);
println!("{}", name);
}
fn length(name_arg: &String) {
name_arg.push_str("ontech");
}

Unfortunately, this program is also invalid. name is mutable, and name_arg is a reference to name. However, name_arg is not mutable. As it turns out, references have the same mutation rules as variables. If they’re not explicitly defined as mutable, they’re immutable by default.

Note that the opposite scenario will also produce an error. This one is probably a bit easier to reason without knowing the reference rules. Immutable variables cannot have mutable references. So this will not compile either.

fn main() {
let name = String::from("laurie");
length(&mut name);
println!("{}", name);
}
fn length(name_arg: &mut String) {
name_arg.push_str("ontech");
}

In order to mutate name_arg we need both the ownership variable and the reference to be mutable.

fn main() {
let mut name = String::from("laurie");
length(&mut name);
println!("{}", name);
}
fn length(name_arg: &mut String) {
name_arg.push_str("ontech");
}

It’s worth mentioning a few other rules.

  1. You can only have one mutable reference per variable in a given scope.
  2. You cannot have both a mutable and an immutable reference to the same variable in a given scope.

Again, these rules allow the Rust compiler to give us a lot of confidence in our code before it’s ever run.

Scope, scope, scope

The thing I’m learning about Rust, like many other languages, is that scope is incredibly important. Given the key concepts of ownership, reference and mutability, the ability to follow the rules requires a clear understanding of your current scope and what else is in it.

An extra example

Early in this post we looked at passing ownership to a function versus using a reference. When you find yourself making those choices it can help you clarify your code. Returning to the original example, if we don’t make use of name again inside of the main function it’s a valid program. However, if that’s the case, the variable name does not need to exist inside the scope of main at all. Instead, we can initialize it in length.

fn main() {
let num_bytes = length();
println!("{}", num_bytes);
}
fn length() -> usize {
let name = String::from("laurie");
name.len()
}

A blog by Laurie Barth.
Teacher of all things tech.