Learning how to code in Rust can feel like a daunting task. While it is a very powerful language it is not precisely known for being a simple language.
The same concepts that make Rust such a great language are the same that may cause problems for newcomers: ownership, lifetimes, among others.
It is not that these concepts are inherently difficult, it's just that they provide safety checks that other languages don't; these safety checks can become an obstacle to people who is familiar with other, more forgiving, languages.
This is why, when someone is writing their first lines of Rust and tries to do simple tasks like passing an object to a function, the compiler will stop them with errors.
Rust compiler relies heavily on static code analysis to find memory errors. While other languages like C or Java allow developers to write code that may result in undesired states (e.g. NullPointerException
and stack overflows) -which then need to be handled during runtime with error handling and exceptions-, Rust will not allow such code to compile.
In this post, we will focus on common problems that any beginner will find when they start their journey into the world of Rust.
Trying to modify immutable variables
In the following example, we create an instance of a struct
called Book
, and we try to assign a different value to its title
property:
struct Book {
title: String
}
fn main() {
let book = Book {
title: String::from("The Rust Programming Language")
};
book.title = String::from("Another book")
}
If we try to compile this code, we will get the following error:
error[E0594]: cannot assign to `book.title`, as `book` is not declared as mutable
Solution
This first problem is easy to spot. By default, all Rust variables are declared as immutable (read-only/constant). The goal of this decision is for developers to be explicit when they expect a variable to change values.
If we want to make book
mutable, we must declare it as let mut book
:
struct Book {
title: String
}
fn main() {
let mut book = Book {
title: String::from("The Rust Programming Language")
};
book.title = String::from("Another book")
}
Another flavor of this same error can be found in the following example:
let x = 1;
x = 2;
error[E0384]: cannot assign twice to immutable variable `x`
The same solution applies: Use let mut x = 1
to allow future assignments to x
.
Trying to use a "moved" variable
Following the previous example, we now define two functions that receive an instance of Book
and try to do something with it (e.g. printing its title):
fn print_book(book: Book) {
println!("Book: {}", book.title);
}
fn do_something_else(book: Book) {
println!("Again: {}", book.title);
}
fn main() {
let book = Book {
title: String::from("The Rust Programming Language")
};
print_book(book);
do_something_else(book);
}
If we try to compile this code, we will find the following error:
error[E0382]: use of moved value: `book`
--> src/main.rs:19:23
|
14 | let book = Book {
| ---- move occurs because `book` has type `Book`,
| which does not implement the `Copy` trait
...
18 | print_book(book);
| ---- value moved here
19 | do_something_else(book);
| ^^^^ value used here after move
Solutions
This problem is caused by Rust's ownership rules around data references.
Rust has no garbage collector like Java, nor it requires developers to manually "destroy" variable references to free the allocated memory. Instead, it uses ownership to define when to clear the memory referred by a variable: When execution reaches the end of a function, all variables owned by that function will go out of scope and their memory will be released.
When the program starts, the main
function has ownership of the book
variable. Then, when we call print_book(book)
, the function print_book
now owns book
. When execution reaches the end of print_book
, book
will be out of scope and its data is cleared.
There are multiple solutions to this:
- Change
print_book
to return ownership ofbook
tomain
. - Change
print_book
to receive a borrowed reference of book.
Solution 1: Returning ownership
The following code will compile correctly:
fn print_book(book: Book) -> Book {
println!("Book: {}", book.title);
book
}
fn do_something_else(book: Book) {
println!("Again: {}", book.title);
}
fn main() {
let mut book = Book {
title: String::from("The Rust Programming Language")
};
book = print_book(book);
do_something_else(book);
}
Notice that we changed book
's declaration to make it mutable, so we can re-assign its value. Also, we updated print_book
to return Book
once it's done with it.
I'm not a big fan of this approach, as I find it verbose and difficult to deal with if print_book
is expected to return something else.
Solution 2: Use a borrowed reference
The following code will compile correctly:
fn print_book(book: &Book) {
println!("Book: {}", book.title);
}
fn do_something_else(book: Book) {
println!("Again: {}", book.title);
}
fn main() {
let book = Book {
title: String::from("The Rust Programming Language")
};
print_book(&book);
do_something_else(book);
}
Notice we updated print_book
to receive an instance of &Book
, which is an immutable reference to an instance of Book
. This is called borrowing. We also changed print_book(book)
to print_book(&book)
, to send a reference of book
to the function.
Now, print_book
never gets ownership of book
: It will receive the read-only reference. When execution reaches the end of print_book
, book
will not go out of scope, as main
still owns it.
Just keep in mind that, in this example, do_something_else
does get ownership of book
, which is perfectly legal.
Another solution is to follow the compiler's advice and implement the Copy
trait for Book
, which then would create copies of book
when passed to other functions. But then we would be using different instances of book
, which has some downsides:
- Extra memory allocation. If
book
is large and we copy it every time we pass it to a function, we are needlessly consuming memory. - Doesn't play well with mutations. If one of the functions is expected to change
book
's contents, then we must return the copy so the next functions can have access tobook
's updated values. This results in the same flow as returning ownership, but with extra steps.
Trying to borrow as both mutable and immutable in the same scope
Now, we will modify the book's title using a new function, and print it with another one. The following code will compile correctly:
fn rename_book(book: &mut Book) {
book.title = String::from("Something else");
}
fn print_title(book: &Book) {
println!("Book: {}", book.title);
}
fn main() {
let mut book = Book {
title: String::from("The Rust Programming Language")
};
rename_book(&mut book);
print_title(&book);
}
However, you will find that doing the following -which seems like it's practically doing the same thing- does not compile:
fn rename_book(book: &mut Book) {
book.title = String::from("Something else");
}
fn print_title(book: &Book) {
println!("Book: {}", book.title);
}
fn main() {
let mut book = Book {
title: String::from("The Rust Programming Language")
};
let mut book1 = &mut book;
let book2 = &book;
rename_book(&mut book1);
print_title(&book2);
}
error[E0502]: cannot borrow `book` as immutable because it is also borrowed as mutable
--> src/main.rs:20:17
|
19 | let mut book1 = &mut book;
| --------- mutable borrow occurs here
20 | let book2 = &book;
| ^^^^^ immutable borrow occurs here
21 |
22 | rename_book(&mut book1);
| ---------- mutable borrow later used here
Aren't we also borrowing as both mutable and immutable in the first example? Not really.
Solution
Rust will not allow us to create mutable and immutable references of an object in the same scope.
In the first example, the actual borrow happens once execution reaches each function rename_book
and print_title
, so there is only one borrowed reference at a time.
While the most obvious solution to this problem would be to use the first example where everything compiles correctly, let us explore a more contrived solution, which will shed some light on the ownership rules:
fn main() {
let mut book = Book {
title: String::from("The Rust Programming Language")
};
{
let mut book1 = &mut book;
rename_book(&mut book1);
}
let book2 = &book;
print_title(&book2);
}
This little workaround makes our code compilable again. When we wrap the borrowed instance of book1
and rename_book
with a block {}
, we are creating a new scope for them. Any variable declared inside the block will go out of scope once execution reaches the block's end. Then, by the time execution reaches let book2
, book1
will not exist anymore and we will only have a single borrow.
Trying to keep a reference to a struct value
Coming from other languages, it's not uncommon for us to try to keep a reference for an object's attribute:
fn main() {
let mut book = Book {
title: String::from("The Rust Programming Language")
};
let title = book.title;
rename_book(&mut book);
println!("Title: {}", title);
}
While this is correct in other languages, Rust will complain:
error[E0382]: borrow of partially moved value: `book`
--> src/main.rs:20:17
|
18 | let title = book.title;
| ---------- value partially moved here
19 |
20 | rename_book(&mut book);
| ^^^^^^^^^ value borrowed here after partial move
Solution
When we do let title = book.title
, we are passing ownership of the title
attribute from book
to the main
function, which means we cannot use book
in rename_book
. We could give title
's ownership back to book
, but that would prevent us from using the title
variable by itself.
To fix this problem, we could clone the title as let title = book.title.clone()
. However, this approach only works if one, title
implements the Clone
trait (which strings do by default), and two, whatever we use title
for doesn't expect to get the actual value of book.title
:
fn rename_book(book: &mut Book) {
book.title = String::from("Something else");
}
fn print_title(book: &Book) {
println!("Title: {}", book.title);
}
fn main() {
let mut book = Book {
title: String::from("The Rust Programming Language")
};
let title = book.title.clone();
rename_book(&mut book);
println!("Title: {}", title);
print_title(&book);
}
Title: The Rust Programming Language
Title: Something else
If we wanted to keep a real reference to book.title
, we must borrow a reference to it, being careful of not creating immutable and mutable borrows in the same scope:
fn main() {
let mut book = Book {
title: String::from("The Rust Programming Language")
};
{
rename_book(&mut book);
}
let title = &book.title;
println!("Title: {}", title);
print_title(&book);
}
Title: Something else
Title: Something else
Of course, this is a very self-contained example that will not work in other cases. For instance, what if we want to keep a reference to book.title
-independent from book
- and return it from a function?
fn create_book() -> (Book, &String) {
let book = Book {
title: String::from("The Rust Programming Language")
};
return (book, &book.title);
}
fn main() {
let (mut book, title) = create_book();
rename_book(&mut book);
println!("{}", &title);
}
The first problem we will find is the following:
error[E0106]: missing lifetime specifier
--> src/main.rs:16:28
|
16 | fn create_book() -> (Book, &String) {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value,
but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
We will not go in-depth on the topic of lifetimes, but what Rust is saying is that, since we're returning a borrowed reference to a String
, the developer needs to add a lifetime to the return type declaration to let Rust know how long is that reference expected to live.
We can try -spoiler-alert, unsuccessfully- to follow Rust's compiler advice and annotate &String
with a 'static
lifetime:
fn create_book() -> (Book, &'static String) {
let book = Book {
title: String::from("The Rust Programming Language")
};
return (book, &book.title);
}
error[E0382]: borrow of moved value: `book`
--> src/main.rs:21:19
|
17 | let book = Book {
| ---- move occurs because `book` has type `Book`, which does not implement the `Copy` trait
...
21 | return (book, &book.title);
| ---- ^^^^^^^^^^^ value borrowed here after move
| |
| value moved here
By the end of the create_book
function, book
is moved out of the function, and ownership is passed to the caller (main()
); because of this, we cannot borrow book.title
(this is the same problem we found in the first example of this post).
Even if we try to store the book title's borrowed reference, we cannot just return a borrowed reference to book.title
:
fn create_book() -> (Book, &'static String) {
let book = Book {
title: String::from("The Rust Programming Language")
};
let title = &book.title;
return (book, title);
}
error[E0515]: cannot return value referencing local data `book.title`
--> src/main.rs:23:12
|
21 | let title = &book.title;
| ----------- `book.title` is borrowed here
22 |
23 | return (book, title);
| ^^^^^^^^^^^^^ returns a value referencing data owned by the current function
Even if it looks like we're passing the ownership of title
to main()
, in reality, we are trying to return a reference to a variable owned by create_book
; such variable will also go out of scope at the end of create_book
.
What options do we have here?
Using Rc to create immutable references
Basic ownership rules work well in simpler applications. However, larger, more complex code will inevitably require getting multiple references to a single object and passing it across functions. For those cases, std::rc::Rc
-Reference Counter- is a handy tool.
Reference counters allow us to create multiple immutable references to a single object. These references can be safely passed from one function to another:
use std::rc::Rc;
struct Book {
title: Rc<String>
}
fn create_book() -> (Book, Rc<String>) {
let book = Book {
title: Rc::new(String::from("The Rust Programming Language"))
};
let title = Rc::clone(&book.title);
return (book, title);
}
fn main() {
let (book, title) = create_book();
println!("{}", book.title);
println!("{}", title);
}
The Rust Programming Language
The Rust Programming Language
Under the hood, Rc
keeps a count of the number of references that have been created. Once all references go out of scope, the object they were pointing at will be removed from memory. In a way, Rc
works as a very simplified Garbage Collector.
This example is a bit misleading, as it looks like we're cloning book.title
's value, when we're cloning a reference to it. This difference is clearer if we update book.title
's value.
Using Rc and RefCell to create mutable references
Reference counting by default is immutable. If we want to update book.title
we must introduce RefCell
to make title
a mutable reference:
use std::rc::Rc;
use std::cell::RefCell;
struct Book {
title: Rc<RefCell<String>>
}
fn create_book() -> (Book, Rc<RefCell<String>>) {
let book = Book {
title: Rc::new(RefCell::new(String::from("The Rust Programming Language")))
};
let title = Rc::clone(&book.title);
return (book, title);
}
fn rename_book(book: &Book) {
book.title.replace(String::from("Something else"));
}
fn main() {
let (book, title) = create_book();
rename_book(&book);
println!("{}", book.title.borrow());
println!("{}", title.borrow());
}
This code will print:
Something else
Something else
With this example, we demonstrate two points:
- The
Rc::clone(&book.title)
returns a clone of the reference tobook.title
, not a clone ofbook.title
's value. Otherwise,println!("{}", title.borrow())
would print the old value"The Rust Programming Language"
. - Using an
Rc<RefCell<String>>
variable type forbook.title
allows us to create copies of its reference and mutate its value.
A warning on Rc and RefCell
While Rc
and RefCell
allow us to create a more complex workflow, they also increase the complexity within your application. Just from the ergonomics point of view, having to pass around variables with <Rc<RefCell<T>>
gets very verbose quickly.
Also, using reference counters when it's not needed increases the risk of memory leaks: If we keep a reference copy for too long, the underlying memory it points to may not be released. In cases where we implement things like HTTP servers using Rust, we might accumulate reference copies to objects that are not used, increasing the amount of consumed memory. This is a common problem in languages like Java, where keeping references to unused variables also lead to memory leaks that the Garbage Collector cannot fix.
When possible, we must avoid overusing these tools and try to work with the basic rules of ownership.
With this last example, we reached the end of this post. However, in future posts we will explore more potential difficulties that beginners find when learning Rust.
Conclusion
Rust ownership and mutability rules are completely different from the way other languages enforce memory safety. While these tools can make it difficult for newcomers to ramp up in their learning journey, they are the reason why Rust is so safe and lightweight.
Not only Ownership allows Rust to operate without tools like Garbage Collectors, but it also forces developers to think deeper into how memory is allocated, used and released in their applications. Languages like C
and C++
offer this level of control too, but without the safeguards that Rust's compiler enforces.
While it seems like the Rust compiler throws too many errors, it is for our own good. It's better to catch these problems during compilation than to try to debug them on execution time, many times in production environments.