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.
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.
- You can only have one mutable reference per variable in a given scope.
- 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()}