Rust’s Unique Memory System - Understanding Ownership and Borrowing



This content originally appeared on DEV Community and was authored by Temitope Aroyewon

When in doubt, remember that Rust prioritizes preventing unexpected changes to data.

Ownership in Rust is a unique concept every Rust developer should be acquainted with. Ownership is a way Rust manages its memory. It lies at the heart of how Rust handles memory safely and efficiently, without needing a garbage collector.
In many other languages, memory management is either done manually by the programmer (like C or C++) or automatically by a garbage collector (like in Java or JavaScript). However, Rust takes a different approach. It introduces a system of ownership, enforced at compile time, to ensure memory safety and performance without needing runtime overhead.

The Basics of Ownership

Each value has a single owner.

fn main() {
  let country_a = String::from("Ireland");
  // country_a goes out of scope here
}

From the above example, countryA is assigned a value of string — Ireland. Which automatically means countryA is the owner of the value. When countryA moves to the second line, it goes out of scope, and Rust automatically cleans up the memory.

A value can only be owned by one variable at a time.

fn main() {
  let country_a = String::from("Ireland");
  let country_b = country_a;

  println!("{}", country_a)

  //Error - borrow of moved value: `country_a`
}

From the example above, the error occurs because country_a ownership is moved to country_b, therefore making country_a’s value invalid. Using country_a afterward causes a compile-time error, which leads us to the next basics.

When the owner goes out of scope, the value is automatically dropped (i.e., the memory is freed).

The Borrow System

The preferred way to work around the requirements of the ownership system, while still creating value that we can freely share and use throughout our application, is by using the borrow system. The borrowing system revolves around how values are owned, borrowed, and used. Borrowing helps you to give temporary access to value without transferring ownership - this act in Rust is called a Reference. To understand the concept of the Borrow System, we need to understand reference concepts:

In Rust, a reference is a mechanism for borrowing a value without taking ownership of it. It lets you access data without transferring or duplicating it. Using references allows multiple functions or parts of code to use the same data without taking ownership. To create a reference to a value, we’re going to use the ampersand operator (&). The ampersand operator can be used on a type and used on the owner of a value.

fn print_country(country: &String) {
    println!("{}", country);
}

fn main() {
  let country_a = String::from("Ireland");
  print_country(&country_a)
}

As seen in the example above, we used the ampersand operator (&) in our argument passed to print_country, which makes it a reference to country_a.

We have two types of references in Rust, namely:

Immutable Reference: This is a reference that allows read-only access. While it can only be used to read a value, it cannot be used to modify one. In an immutable reference, a value cannot be moved or changed while the reference to the value still exists. Let’s discuss an example below:

fn main() {
  let animal = String::from("Dog");
  let other_animal = &animal;
  println!("{}", animal);

  let move_animal = animal;
  println!("{}", animal, move_animal);
}
// error[E0382]: borrow of moved value: `animal`

In the example above, an error occurs because we are trying to move the value of animal after it has already been borrowed immutably by other_animal. The borrow system ensures that, as long as an immutable reference (&animal) exists, the original value cannot be moved or mutated. Now, let’s look at the second kind of reference in Rust.

Mutable Reference: This is a reference that allows read and write access to a value. It is also known as a writable reference. Mutable reference allows us to read and change the value without moving the value. When writing code that requires mutable references, there are certain rules that must be followed:

  • You can have only one mutable reference at a time.
  • You cannot have a mutable reference while immutable references exist.

We can write a mutable reference by adding the ampersand and the mut keyword (&mut).

#[derive(Debug)]
struct Person {
    name: String,
    place_of_birth: String,
}

impl Person {
    fn new(name: String, place_of_birth: String) -> Self {
        Self { name, place_of_birth } 
    }
}

fn change_person(person: &mut Person) {
    person.name = String::from("Temitope");
}

fn main() {
  let mut person_a = Person::new(String::from("Aroyewon"), String::from("Nigeria"));
  change_person(&mut person_a);

  println!("{:#?}", person_a);
}

From the example above, we define a Person struct with two fields: name and place_of_birth. We implement a new method to create a new Person instance easily. Our change_person function takes a mutable reference to a Person and updates the name field with the introduction of the &mut keyword. In our main function, we create a mutable version of person_a, pass it to the change_person function, and then print the updated struct. We should note that person_a is allowed to be updated because of the introduction of a mutable value, which was done on line 19.

However, despite having mutable and immutable references, some types of values are only copied instead of moved. This means that there are some types of values in Rust that do not follow the rules and guides of ownership. Examples of such values include: numbers, char, boolean, array/tuples with copyable elements. This means certain types of values behave differently and do not follow the standard ownership rules.

fn main() {
    let num = 30;
    let other_num = num;
    println!("{} {}", num, other_num);
}

In summary, whenever you’re in doubt, remember that Rust is built with safety and predictability in mind, especially when it comes to how data is accessed and modified. One of Rust’s core principles is to prevent unexpected or accidental updates to data, which can lead to bugs or hard-to-track behavior in your code.

📚 References

  1. The Rust Programming Language Book – https://doc.rust-lang.org/book
  2. Rust by Example – https://doc.rust-lang.org/rust-by-example
  3. Rust: The Complete Developer’s Guide by Stephen Grider- Udemy (Paid Course)


This content originally appeared on DEV Community and was authored by Temitope Aroyewon