Over the past few months I've been using Rust A LOT and I wanted to post a casual article explaining some of the oddities of Rust that I struggled with coming from C as a way to give back to the community and help others. Huge thanks to everyone on this board for helping me in understanding these concepts!
Who should read this article?
You should read this article if:
- You are new to Rust and struggling understanding the "move semantics" and borrow system
- You find yourself practically ripping your hair out over compiler errors concerning mutability, borrows, and moves
- You are already a programmer or know at least one other language well, especially a low-level/native language
I highly discourage you from reading this article if you are not already a programmer or are new to computer programming. Rust is a difficult language to learn and I honestly cannot recommend it as a first language at this point in time due to limited learning resources and complex subjects which are learned much easier with in-depth knowledge of how computer memory works. If you choose to continue anyway, don't let Rust discourage you from an awesome learning experience in computer programming.
Introduction
I will openly state that learning Rust was the most steep learning curve for me than any other programming language, ever. C++ would be a close second, but that is out of x86-64 assembly, C, C#, Python, PHP, JavaScript, CSS (although I still hate CSS), SQL, and probably others that I forgot to mention.
But why? What makes Rust so difficult to learn?
Well, in other languages, there are conventions... Sure, JavaScript and C are very different, Python and C# are very different, but in each of those languages, it is pretty standard to be able to pass data around freely between functions without having to reason about when the data is freed, if it's freed, accidentally referencing uninitialized data, sending the program into corrupted memory, etc...
However, most of those languages also feature a runtime environment which runs alongside your program and keeps it in check via a garbage collector and exception handling. This is all fine and good except there is a large runtime performance cost, the system's complexity is increased, and the programmer loses some low-level control over the program's memory.
On the other hand, lower-level languages such as C and C++ just let you shoot yourself in the foot - meaning, they won't stop you from writing code with a ton of memory errors in it - but your program will suddenly crash with a not-so-descriptive error message. This leads to programmers spending hours and hours of time in a debugging stepping through the code trying to figure out what went wrong... And if it's not at source-level, then may even have to single-step through assembly instructions, such as when a compiler update modifies the behavior of the output all things left the same in the source code.
Rust takes a much different approach - in exchange for a steeper learning curve, going to hell and back at war with the compiler at first, and in some cases, slightly more verbose code, the programmer gets a 100% guarantee that his/her code is completely free of ANY AND ALL memory bugs. Memory bugs include buffer overflows, use-after-free, derefencing unitialized data, null ptr dereference, double free, and many other memory errors which would ordinarily lead to security vulnerabilities, crashed programs, and unhappy users. In fact, a great deal of money is spent on so-called static analysis tools as a response to the aforementioned problems, which are completely prevented in safe Rust.
Note, I said "Safe Rust." Rust does have the option to use unsafe
blocks, and what you do in there, there are no memory safety guarantees. The good news is, you rarely should need to use unsafe blocks and as the language matures, it will likely mean that unsafe code will be necessary less and less.
To Hell and Back with the Compiler - Moving and Borrowing
Now, let's learn you some Rust. If you've already tried writing some Rust and building it, I promise you that you've seen errors about mutability, moving, and borrowing. It is inevitable... In fact, I wouldn't be surprised if you've seen these errors thousands of times if you are new to the language.... And began to question why you are even using Rust, why you bothered trying to learn it, and perhaps even questioning your sanity.
For this reason, I'm going to take an absolute no-nonsense, start-from-scratch approach at explaining moving, borrowing, the differences between them, the errors you will see, what they mean, and how to handle all of it. Yes, there is the Rust Book, and there are a few other resources online here and there, but in this post, I'm going to really try and make this "idiot-proof" and spare some of the super technical jargon in favor of helping the reader understand all of this stuff to begin with. That being said, this article is on the usage of Rust as a programmer, not the implementation of the language. I could get into the technical details of how the language is actually implemented and what is going on behind the scenes but that would do far more harm than good and is a task for a much later time when you know moving and borrowing like the back of your hand first.
TLDR - Agenda:
- You will learn Moving and Borrowing
- You will not learn about lifetime specifiers - that will be the next article
- You will be looking at and working with a lot of code- all examples will have runnable links provided. In fact, you don't even need Rust installed!
- We are using an informal voice in favor of understanding
Mutability
First and foremost, the most common error a new Rust programmer will probably discover is that the following code will generate error:
error[E0384]: cannot assign twice to immutable variable
name
fn main()
{
let name = "Thomas";
name = "Jane";
println!("Name is: {}", name);
}
In Rust, all variables which are assigned using a let
statement are immutable unless otherwise specified. Immutable is a fancy term which means:
You cannot change my value once I have been initially set.
Since we've tried to change the immutable variable name
to Jane after it was set to Thomas, we receive that error.
To fix this error, simple add mut
before name = "Thomas";
like so:
fn main()
{
let mut name = "Thomas";
name = "Jane";
println!("Name is: {}", name);
}
References
Just like in C or C++, we can create a reference or pointer to data... Meaning, we can store the address of where a variable is in another variable. Observe:
fn main()
{
let mut name = "Thomas";
name = "Jane";
let name_ref = &name;
println!("Name is: {}", name);
}
Here, name_ref
is an immutable reference to name
.
WHOA what??? I thought name was mutable now???
CONFUSION ALERT - The reference type is completely independant of the original variable's data type... This means we can create an immutable reference to a mutable variable, (shown above). However, we cannot create a mutable reference to an immutable variable. Meaning, while the above is totally legal, the below will generate error:
error[E0596]: cannot borrow immutable local variable
name
as mutable
fn main()
{
let name = "Thomas";
let name_ref = &mut name;
}
Why not?
If you look closely, you'll notice that creating a mutable reference to immutable data would make no sense - we cannot mutate the data, so why would we need a mutable reference?
On the other hand, creating an immutable reference to mutable data makes perfect sense - maybe we want a function to read data but not be able to change it... Perfectly legal and has many use-cases.
In summary: We can create immutable references to mutable variables as well as immutable variables, but we can only create mutable references to mutable variables.
Now, let's introduce a complex type that we will use for the remainder of the article in demonstrations:
Player struct
This article is meant to show real-world examples of code, not just as basic crap as possible, hence the recommendation of already being a programmer for the intended audience.
struct Player
{
name: String,
age: u8,
description: String
}
Here we have a complex type called "struct Player" which has two Strings and a unsigned 8 bit integer. Pretty basic.
Now, we're going to new up a Player object like so:
let mut our_player = Player
{
name: "Jones".to_string(), //converts &str (static string) to a String (heap memory)
age: 25,
description: "Just a happy guy.".to_string()
};
What the hell is a "move?"
In Rust, a move means that data's ownership has been handed over from one function or method to another. It's very similar to automobile ownership. With cars, the owner holds the car and the title. Once he transfers the car and title to someone else, he no longer owns the car and he does not have access or control over it. Now, let's look at some code:
struct Player
{
name: String,
age: u8,
description: String
}
fn main()
{
let mut our_player = Player
{
name: "Jones".to_string(),
age: 25,
description: "Just a happy guy.".to_string()
};
mover(our_player);
println!("My name is {}, and I am being used after a move", our_player.name);
}
fn mover(moved: Player) -> Player
{
println!("I am {}, I've been moved into mover", moved.name);
moved
}
The above code generates this compiler error:
error[E0382]: use of moved value:
our_player.name
WHY???
This error is generated due to two lines of code:
Line 1: fn mover(moved: Player) -> Player
, the function signature.
Line 2: mover(our_player);
, the function call
Specifically, since we specify that mover()
takes a Player and not a &Player (immutable reference to Player), Rust transfers ownership of our_player
to mover()
permanently. This means that main()
no longer has any control over or access to our_player
! WHOA... Much different than other programming languages where our_player
would simply be "passed by value" here.
To fix this, we change the function signature to accept a &Player
and we pass in &our_player
instead of our_player
. Make sense??? See below:
struct Player
{
name: String,
age: u8,
description: String
}
fn main()
{
let mut our_player = Player
{
name: "Jones".to_string(),
age: 25,
description: "Just a happy guy.".to_string()
};
immutable_borrow(&our_player);
println!("My name is {}, and I am being used after an immutable borrow", our_player.name);
}
fn immutable_borrow(borrowed: &Player)
{
println!("I am {}, I've been immutably borrowed", borrowed.name);
}
Notice the delta here - I implemented what I described before the last code block but in order to not confuse the reader, I've changed the function name from mover()
to immutably_borrow
and changed the text inside of the println!
to reflect that.
BUT WAIT - YOU WILL PULL YOUR HAIR OUT OVER THIS!!!!
So, check out this code, which happens to compile just fine:
fn main()
{
let age: u8 = 55;
mover(age);
println!("The age is: {}", age);
}
fn mover(age: u8) -> u8
{
println!("Age {} has been moved into mover!", age);
age
}
WHAT THE &$(@&#(@#&?????
Didn't I just show that when data is passed into a function without using a &
reference, it is moved and thus ownership has been transferred out of main()
(or some other calling function) and cannot be accessed? Thus, println!()
should not work because main()
no longer owns age
, right?
WRONG!!!
So it turns out, Rust has a nasty little secret that is not super obvious upfront... It has a Trait called Copy
and any data type which implements that Trait, the Rust compiler does not move. Instead, it copies the data into another function or variable, much like the typical "pass by value" behavior in other languages. There's no syntactic way to tell this at first. However, if you were to study the Rust documentation, you could eventually figure this out... But at first, it just looks like total inconsistency on the compiler's part because with some types, such as our Player
type, it would move the data and generate the error, while other types like the u8, u16, u32, and all other "primitive types," it would copy and compile just fine!
So the trick here is before you get annoyed about why or why not you are getting compiler errors about moves, check and see if the data type you are trying to move implements the Copy
trait. Player
does not and neither does the standard String
type.
There is no mutable/immutable move
A move is simply a move, that's it. Once data has been moved, it is completely owned by whomever it is moved to and the owner is free to mutate, read, or destroy it (when the current scope/block ends). Rust automatically destroys data for you, so you won't be seeing any delete
s.
What on earth is a "borrow?"
Unlike a move, a borrow, as we already introduced in a code example above, is when you pass a reference to data rather than the data itself using the &
. However, there are two types of borrows and we've only shown one thus far:
Immutable Borrows
This is what we demonstrated above. Just to review:
immutable_borrow(&our_player);
println!("My name is {}, and I am being used after an immutable borrow", our_player.name);
}
fn immutable_borrow(borrowed: &Player)
{
println!("I am {}, I've been immutably borrowed", borrowed.name);
}
That code illustrates an immutable borrow... You can pass around as many of these as you like, as long as there are no mutable borrows in scope. So what is a mutable borrow and how is it different? An immutable borrow allows whomever is borrowing it to read only and not modify the data. A mutable borrow allows whomever is borrowing it to actually modify (mutate) the data. As you may guess, mutable borrows introduce much more complexity and are policed by the compiler much more than immutable borrows are.
Mutable Borrows
Look at the following code:
struct Player
{
name: String,
age: u8,
description: String
}
fn main()
{
let mut our_player = Player
{
name: "Jones".to_string(),
age: 25,
description: "Just a happy guy.".to_string()
};
immutable_borrow(&our_player);
change_name(&mut our_player);
println!("My name is {}, and I am being used after an mutable borrow", our_player.name);
}
fn immutable_borrow(borrowed: &Player)
{
println!("I am {}, I've been immutably borrowed", borrowed.name);
}
fn change_name(borrowed: &mut Player)
{
borrowed.name = "My New Name".to_string();
}
I want you to pay close attention to the syntactic differences between an immutable borrow and a mutable one. Here, our mutable borrow occurs in the function change_name()
which does as it says - changes the name of our_player
. First, let's look at the function signature: fn change_name(borrowed: &mut Player)
Notice, instead of Player
, or &Player
, we use &mut Player
. This tells the Rust compiler that we are preparing to accept a mutable borrow.
Also notice that we must also specify it in every single call explicitly: change_name(&mut our_player);
We cannot just call change_name(our_player)
and we also cannot call change_name(&our_player)
. We must call change_name(&mut our_player)
.
This is also a big gotcha for newbies, they will specify the proper function signature, but forget the &mut
when they call the function.
Now, let's talk about semantics. How is a mutable borrow treated differently than an immutable borrow? Well, as you may imagine, ** (safe) Rust only allows one mutable reference to data in scope at any given time, period, no questions asked.** I say safe Rust because this can be overriden by using unsafe
blocks which we are not covering here and should be avoided if at all possible.
**Also note that Rust will not allow ANY immutable references to data when there is an active mutable reference! **This is to prevent data races which occur when one piece of code is trying to read the data while another is writing to it - these introduce nasty bugs.
Let's view an example of code which will not compile:
struct Player
{
name: String,
age: u8,
description: String
}
fn main()
{
let mut our_player = Player
{
name: "Jones".to_string(),
age: 25,
description: "Just a happy guy.".to_string()
};
let my_immutable_return = immutable_borrow(&our_player);
change_name(&mut our_player);
println!("My name is {}, and I am being used after an mutable borrow", our_player.name);
}
fn immutable_borrow(borrowed: &Player) -> &Player
{
println!("I am {}, I've been immutably borrowed", borrowed.name);
borrowed
}
fn change_name(borrowed: &mut Player)
{
borrowed.name = "My New Name".to_string();
}
Here, we've immutably borrowed our_player
into my_immutable_return
. We've then tried to mutably borrow our_player
while it is stilled immutably borrowed.
Wait, what???
Yes, so when we create a borrow like this:
let my_immutable_return = immutable_borrow(&our_player);
It does not get "returned to its owner" until the curly brace after it. The problem here is that my_immutable_return
has borrowed my_player
until the curly brace at the end of the main()
function. However, before we've reached the end of main()
, we've attempted to mutably borrow our_player
. We get this error:
error[E0502]: cannot borrow
our_player
as mutable because it is also borrowed as immutable
All borrows in Rust end at the end of the "code block" aka the curly brace following the borrow.
This means that the following code compiles and runs just fine:
struct Player
{
name: String,
age: u8,
description: String
}
fn main()
{
let mut our_player = Player
{
name: "Jones".to_string(),
age: 25,
description: "Just a happy guy.".to_string()
};
{
let my_immutable_return = immutable_borrow(&our_player);
}
change_name(&mut our_player);
println!("My name is {}, and I am being used after an mutable borrow", our_player.name);
}
fn immutable_borrow(borrowed: &Player) -> &Player
{
println!("I am {}, I've been immutably borrowed", borrowed.name);
borrowed
}
fn change_name(borrowed: &mut Player)
{
borrowed.name = "My New Name".to_string();
}
The only change I made is that I added curly braces around let my_immutable_return = immutable_borrow(&our_player);
which caused the borrow to end on line 18 of the Rust playground (click Try me above). Because the borrow ended earlier, now, we are free to mutably borrow our_player
again.
Moving and Borrowing summary
One critical error that many new Rust programmers can make coming from languages like C and C++ is that we see &
and we think "pass by reference" and when we see no &
we think "pass by value." This will cause you to freak out a lot... I've found that the best way to think as a Rustacean is instead to see &
and think "pass by borrow" and when we use no &
we are "passing by move."
Whew, that was a lot... Please do experiment yourself in the Playground examples... Create more compiler errors, see what happens when you try to borrow twice and three times, try to move twice, etc... It takes some getting used to.
Other common pitfalls that will destroy you
I want to show you something else that has to do with Structs in Rust which are similar (but not the same) as classes in other languages. Rust does have methods. See below:
struct Player
{
name: String,
age: u8,
description: String
}
impl Player // This just means "define methods on Player"
{
fn print_me(self)
{
println!("Name: {}\nAge: {}\nDescription:{}", self.name, self.age, self.description);
}
}
fn main()
{
let mut our_player = Player
{
name: "Jones".to_string(),
age: 25,
description: "Just a happy guy.".to_string()
};
our_player.print_me();
}
This code runs just fine... In fact, it's pretty easy to understand and very Python-like, huh?
But let's call our_player.print_me()
again:
struct Player
{
name: String,
age: u8,
description: String
}
impl Player // This just means "define methods on Player"
{
fn print_me(self)
{
println!("Name: {}\nAge: {}\nDescription:{}", self.name, self.age, self.description);
}
}
fn main()
{
let mut our_player = Player
{
name: "Jones".to_string(),
age: 25,
description: "Just a happy guy.".to_string()
};
our_player.print_me();
our_player.print_me();
}
BANG!!! Doesn't compile!!!:
error[E0382]: use of moved value: `our_player`
--> src/main.rs:26:5
|
25 | our_player.print_me();
| ---------- value moved here
26 | our_player.print_me();
| ^^^^^^^^^^ value used here after move
What on earth?????
So, in Rust, turns out that the way we've defined our print_me()
method, we've specified self
. Seems legit, right???
WRONG!
What's actually happening here is a bit perplexing at first, but we've moved the our_player
object into its own print_me()
method, and then discarded it on the first call to our_player.print_me()
such that when we try to call it again, print_me()
cannot access its own parent object!
Luckily, although this is a nasty error that can be tough to understand, it has a very simple fix... We change the method signature from fn print_me(self)
to fn print_me(&self)
.
That's right - one single &
fixes this problem... Now, we've immutably borrowed the parent object so that it is not permanently moved into its own print_me()
method.
I'd like to also point out that here print_me()
does nothing other than read the data inside of our_player
but if it were to change the data, we would get another error again because we've only passed an immutable reference to the function. To resolve this, we would need to change the signature to fn print_me(&mut self)
. I hope this is starting to all make sense now... If we need to modify the data, we need a mutable reference to it, even in the case of struct objects. Let's look at one more example to solidify this:
struct Player
{
name: String,
age: u8,
description: String
}
impl Player // This just means "define methods on Player"
{
fn print_me(&self)
{
println!("Name: {}\nAge: {}\nDescription:{}", self.name, self.age, self.description);
}
fn change_me(&mut self)
{
self.name = "changed".to_string();
self.age = 240;
}
}
fn main()
{
let mut our_player = Player
{
name: "Jones".to_string(),
age: 25,
description: "Just a happy guy.".to_string()
};
our_player.print_me();
our_player.change_me();
our_player.print_me();
}
And finally we get the output:
Name: Jones
Age: 25
Description:Just a happy guy.
Name: changed
Age: 240
Description:Just a happy guy.
Bonus Trait Jargon - Is it Send????
I'd like to point out one last thing - you will often see Rustaceans saying things like:
Is Player Display?
Is u32 Copy?
MyCoolType is Display, Copy, and Clone
This is shorthand for:
Does the Player type implement the Display trait?
Does the u32 type implement the Copy trait?
MyCoolType implements the Display, Copy, and Clone traits
That's all folks - I hope this has really helped clear up some confusion of the Rust move semantics and borrow system. May update this post, and experienced Rustaceans feel free to drop feedback.