DEV Community

Cover image for Common Rust problems for beginners: Ownership & mutability
Pedro F Marquez
Pedro F Marquez

Posted on • Originally published at pedromarquez.dev

Common Rust problems for beginners: Ownership & mutability

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")
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

Another flavor of this same error can be found in the following example:

let x = 1;
x = 2;
Enter fullscreen mode Exit fullscreen mode
error[E0384]: cannot assign twice to immutable variable `x`
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 of book to main.
  • 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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 to book'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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
Title: The Rust Programming Language
Title: Something else
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
Title: Something else
Title: Something else
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
The Rust Programming Language
The Rust Programming Language
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

This code will print:

Something else
Something else
Enter fullscreen mode Exit fullscreen mode

With this example, we demonstrate two points:

  • The Rc::clone(&book.title) returns a clone of the reference to book.title, not a clone of book.title's value. Otherwise, println!("{}", title.borrow()) would print the old value "The Rust Programming Language".
  • Using an Rc<RefCell<String>> variable type for book.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.

Top comments (0)