Fun Guides: Ownership and Borrowing

A fun but comprehensive guide for Ownership in Rust with metaphors from the world of Danny Ocean.

The Rules of Ownership

There are three core rules of ownership in Rust:

  1. Each value in Rust has a variable that's called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
// Rule 1: Each value in Rust has a variable that's called its owner
let owner1 = "Some value";

// Rule 2: There can only be one owner at a time
let owner2 = owner1; // Now owner2 is the owner, and owner1 no longer has access

// Rule 3: When the owner goes out of scope, the value will be dropped
{
    let owner3 = "Another value";
    // owner3 is in scope
} // owner3 goes out of scope here, and "Another value" is dropped

Borrowing

Borrowing allows multiple parts of your code to access data without taking ownership. It's like team members sharing the heist plan document without putting it into their pockets and without changing it.

Immutable borrowing is made with the & symbol.

Ryan lets other members have a look at the heist plan:

fn main() {
    let ryans_plan = String::from("The Heist Plan");

    let linus_looking = &ryans_plan; // Linus reads the plan

    println!("Plan details: {}", linus_looking);
}

Here, linus_looking is a reference to ryans_plan. Linus borrows the plan to read it but doesn't own it nor can he change it.

Let's make Linus more professional and have him explore the plan in a function, again without taking ownership:

fn analyze_plan(plan: &str) {
    println!("Analyzing borrowed plan: {}", plan);
}

fn main() {
    let heist_plan = String::from("The Heist Plan");

    // Borrow heist_plan temporarily:
    analyze_plan(&heist_plan);

    // The original heist_plan wasn't consumed by analyze_plan and can still be used:
    println!("Original plan: {:?}", heist_plan);
}

In this example, the analyze_plan function takes a shared reference &str as a parameter. By passing &heist_plan to the function, we borrow the value of heist_plan without transferring ownership. After the function call, we can still use heist_plan because we retained its ownership.

Mutable Borrowing

Mutable borrowing allows the original to be modified without giving away the ownership of it.

Important: When a mutable borrow is active, there cannot be any other borrow at the same time (whether mutable or immutable).

Mutable borrowing is made with the &mut symbol.

For example, the team decides to make a last-minute change to the plan. Only one person can take the pen and adjust the document at a time and nobody can observe while the change is being made. Basher takes the initiative:

fn main() {
    let mut heist_plan = String::from("The Heist Plan");

    let basher_will_modify = &mut heist_plan; // Basher will modify the plan

    basher_will_modify.push_str(" with a Twist");

    println!("Updated Original: {:?}", heist_plan);
}

Now, basher_will_modify is a mutable reference to heist_plan, allowing Basher to modify it. The original must also be mutable, so notice we defined it with let mut heist_plan.

No one else can borrow heist_plan while it's being changed because of the Rules of Borrowing.

The Rules of Borrowing

Borrowing must follow two main rules so that memory stays safe.

The first rule states that you can have any number of immutable references (&T) to a resource, or you can have one mutable reference (&mut T) at any given time, but not both simultaneously.

fn main() {
    let mut heist_plan = String::from("The Heist Plan");

    let plan_for_discussion = &heist_plan; // Immutable reference
    let second_discussion = &heist_plan; // Another immutable reference is fine

    // The next line would error out: can't borrow `heist_plan` as mutable because it's also borrowed as immutable
    // let plan_for_modification = &mut heist_plan;

    println!("Plan for discussion: {}", plan_for_discussion);
}

The second rule states that references must always be valid.

Just as Danny Ocean would never rely on a team member who isn't dependable, Rust ensures that a reference never outlives the data it points to. This prevents dangling references, which could lead to security vulnerabilities.

In our analogy, if a team member leaves the crew, they can no longer participate in discussions or decisions about the heist.

fn main() {
    let plan_for_modification;

    { // An inner scope starts
        let heist_plan = String::from("The Heist Plan");

        // Let's point plan_for_modification to heist_plan:
        plan_for_modification = &heist_plan;
    } // heist_plan goes out of scope and is dropped here

    // The next line would error out: plan_for_modification contains a reference to a dropped value
    // println!("Modified Plan: {}", plan_for_modification);
}

By following these rules, Rust's borrowing system ensures that memory management is efficient and safe.

Lifetimes

Lifetimes in Rust are like the timeline of each member's involvement in the heist. They ensure that all references are valid for as long as they are needed, and no longer.

Let's consider the next scenario where we need to compare two plans:

fn main() {
    let ryans_plan = String::from("Hack the security system");
    let linuses_plan = String::from("Pickpocket the key card");

    let best_plan = compare_plans(&ryans_plan, &linuses_plan);

    println!("The best plan is: {}", best_plan);
}

Now we will implement the compare_plans function without lifetimes:

// Without lifetimes - won't work
fn compare_plans(plan1: &str, plan2: &str) -> &str {
    if plan1.len() > plan2.len() {
        plan1
    } else {
        plan2
    }
}

When we run the program, we will get an error. The compiler doesn't know how long the function's return value should live.

So we need to tell the compiler how long should the return reference live with respect to the parameters. We will do so with lifetimes.

// With lifetime 'a
fn compare_plans<'a>(plan1: &'a str, plan2: &'a str) -> &'a str {
    if plan1.len() > plan2.len() {
        plan1
    } else {
        plan2
    }
}

The 'a annotation is called a lifetime specifier. It's not a specific named lifetime but rather a placeholder that the compiler uses to ensure the references have a consistent lifetime. Notice that it is repeated 4 times in the function signature:

  • First at the end of the function name within square brackets. Those square brackets are the space where we specify all lifetime specifiers used in that function. It's just the one named a, so it looks like this: <'a>. But if there were more lifetime specifiers, all would be listed there, such as <'a, 'b, 'c>, and so on.
  • Twice for our two parameters. The type of the parameters plan1 and plan2 would originally be &str but they changed to &'a str to indicate that the parameter should live for at least a particular amount of time, which we codenamed a.
  • And lastly, the value we reference in the function's return should also live at least that long, so -> &str changes to -> &'a str.