Ownership and borrowing are fundamental concepts in Rust that help ensure memory safety and prevent common programming errors like null pointer dereferences and data races. These concepts are essential for understanding how Rust manages memory efficiently and safely.

Ownership Rules

In Rust, each value has an owner, and there can only be one owner at a time. When the owner goes out of scope, Rust will automatically clean up the associated memory.

Stack and Heap

In Rust, data is stored either on the stack or the heap. The stack is for data with known, fixed size, while the heap is for data with an unknown size at compiler time or data that might change in size. The stack operates in a last-in, first-out (LIFO) manner, like a stack of plates, while the heap is less organised.

String Type

Rust provides a String type that allows for mutable, growable text data. Unlike string literals, String is stored on the heap. Strings can be mutated and are more flexible than string literals:

let mut s = String::from("hello");
s.push_str(" world :)");
println!("{}", s);

Move and Ownership

When you assign one variable to another in Rust, ownership is transferred. If a type implements the Copy trait and is stored on the stack, a shallow copy is made. Otherwise, ownership is moved.

let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2

Clone

If you want to create a deep copy of data on the heap, you can use the clone method. This creates a new allocation in the heap, allowing both variables to be valid.

let s1 = String::from("hello");
let s2 = s1.clone(); // creates a deep copy

Stack-Only Data: Copy Trait

Some types, like integers, implement the Copy trait, allowing variables to be copied instead of moved. This is because they have a known, fixed size:

let x = 5;
let y = x; // x is copied, not moved

Functions and Ownership

When passing variables to functions, ownership is transferred unless the type implements the Copy trait. Once a variable is moved into a function, it goes out of scope and is automatically cleaned up when the function exits.

fn takes_ownership(some_string: String) {
    // some_string takes ownership
}
 
fn makes_copy(some_integer: i32) {
    // some_integer is copied
}

Returning Ownership

Functions can return ownership of values to the calling code. This allows for reusing data without moving it. If a value is moved to a function and returned, it transfers ownership back.

fn gives_ownership() -> String {
    // ownership is transferred to the calling code
}
 
fn takes_and_gives_back(a_string: String) -> String {
    // a_string is returned, transferring ownership
}

Borrowing

Borrowing allows functions to access data without taking ownership. It uses references, denoted by &. There are two types of borrowing:

  • Mutable References: &mut, allowing the function to modify data.
  • Immutable References: &, ensuring data remains read-only.
fn calculate_length(s: &String) -> usize {
    // s is an immutable reference
}
 
fn change(s: &mut String) {
    // s is a mutable reference
}

Reference