Saltar a contenido

06 - Ownership and Borrowing

What this session is

This is the Rust page. Plan to spend more time here. It's not actually hard once you see the model; the trouble is that no other mainstream language works this way, so the rules feel arbitrary until they click. Read this page twice if you need to.

The payoff: every error message that confused you in chapter 02 about strings starts making sense, and you'll be able to read Rust function signatures.

The problem ownership solves

In C and C++, you manage memory by hand. You allocate, you free. Easy to forget; easy to double-free; easy to use-after-free. Many bugs and security vulnerabilities.

In Python, Java, Go, you have a garbage collector. The runtime tracks references and frees memory when nothing points at it anymore. Easy on the programmer; expensive in some cases (pause times, memory overhead).

Rust does neither. The compiler tracks at compile time, via a small set of rules, when each value should be freed. No garbage collector; no manual free. This is ownership.

The three rules

Every value in Rust has an owner - a variable. The rules:

  1. Each value has exactly one owner.
  2. When the owner goes out of scope, the value is dropped (freed).
  3. Values can be moved or borrowed, but the ownership rules are checked at compile time.

That's it. Most of the complexity is corollaries of these three rules.

Drop happens at the end of scope

fn main() {
    {
        let s = String::from("hello");
        println!("{s}");
    }   // s goes out of scope here; the String's memory is freed
}

s owns the String. At the closing } of s's scope, Rust automatically calls a destructor that frees the heap memory. No free(). No delete. No garbage collector.

This is automatic memory management without a runtime cost. You don't manage memory; the compiler does, deterministically.

Move semantics: assignment can transfer ownership

let s1 = String::from("hello");
let s2 = s1;            // ownership of the String MOVES to s2
println!("{s2}");       // OK
println!("{s1}");       // COMPILE ERROR: borrow of moved value: `s1`

When you assign s2 = s1, the value (the String) is moved to s2. s1 is no longer valid - using it is a compile error.

This is rule 1: each value has one owner. After let s2 = s1;, the owner is s2. The compiler tracks this.

Why? If both s1 and s2 could access the String, freeing it would be ambiguous - when do you free? In C++ this is the source of double-free bugs. Rust avoids it by transferring ownership unambiguously.

Some types are Copy: assignment copies, doesn't move

Primitives are cheap to copy and don't allocate, so they implement the Copy trait. Assignment copies them; the original stays valid:

let x = 5;
let y = x;
println!("{x}");      // 5 - OK, x is still valid
println!("{y}");      // 5

All the primitive number types, bool, char, and tuples of Copy types are Copy. String, Vec, your structs (by default) are not Copy - they move.

When you read let y = x;, ask: "is x a Copy type?" If yes, x is still valid. If no, ownership moved to y and x is gone.

Functions move (or copy) their arguments

fn take_string(s: String) {
    println!("{s}");
}   // s dropped here

fn main() {
    let s = String::from("hello");
    take_string(s);
    println!("{s}");    // COMPILE ERROR: borrow of moved value
}

When you pass s to take_string, ownership moves into the function. After the call, s is gone - you can't use it. Inside the function, s is dropped at the end of the function's scope.

If you want to use the value after the call, you have two options:

  1. Return ownership. The function takes the value and gives it back:

    fn take_and_return(s: String) -> String {
        println!("{s}");
        s        // return ownership
    }
    
    Awkward. Almost nobody does this.

  2. Borrow instead of taking ownership. This is the everyday answer.

Borrowing: references

A reference is a way to "use this value temporarily without taking ownership." Mark a parameter with & to borrow:

fn print_it(s: &String) {
    println!("{s}");
}   // s here is a reference; the String it points to is NOT dropped

fn main() {
    let s = String::from("hello");
    print_it(&s);             // pass a reference
    println!("{s}");          // s is still valid here!
}

&s creates a reference to the String - a temporary pointer that doesn't transfer ownership. The function works with the reference; the String remains owned by main.

This is borrowing. Read the syntax: - &T - an immutable reference to a T. - &mut T - a mutable reference (can modify the borrowed value).

The borrowing rules (also three)

Rust enforces these at compile time:

  1. At any given time, you can have either:
  2. One mutable reference (&mut T), or
  3. Any number of immutable references (&T).
  4. References must always be valid (the value they point to must outlive them - no dangling pointers).
  5. (Variant: mutable references and immutable references can't coexist for the same value at the same time.)

These rules prevent data races at compile time. If two parts of your code could simultaneously modify the same value, they'd need two mutable references - which is forbidden. Therefore: no data races in safe Rust.

let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1}, {r2}");   // OK - multiple immutable references

let r3 = &mut s;          // COMPILE ERROR if r1 or r2 are used after this point

The "after this point" is important - Rust looks at where each reference is last used. If r1 and r2 are no longer used by the time r3 is created, you're fine.

Mutable references

fn append_world(s: &mut String) {
    s.push_str(", world");
}

fn main() {
    let mut s = String::from("hello");
    append_world(&mut s);
    println!("{s}");        // hello, world
}

The function takes &mut String - a mutable reference. The caller passes &mut s. Two muts: the variable is mutable (let mut s), and the reference is mutable (&mut s).

You cannot have two &mut to the same value at the same time:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;   // COMPILE ERROR
println!("{r1}, {r2}");

This prevents two parts of your code from racing on the same mutation.

&str and String revisited

Now &str makes sense: it's a borrowed reference to text owned by something else. The "something else" is usually:

  • A string literal ("hello"), which lives in the program's static memory.
  • A String (you can borrow a &str view of any String).
let owned: String = String::from("hello");
let slice: &str = &owned;       // borrow a &str view of the String
println!("{slice}");

&str is shorter to type, doesn't transfer ownership, lets functions accept either literals or strings. Most function parameters that take "some text" use &str.

Function parameters: what to choose

A rule of thumb:

  • &str when you just want to read text (most common).
  • &String - almost never. Use &str instead; String coerces to &str automatically.
  • String when you need to take ownership (rare). E.g., a function that stores the string somewhere.
  • &mut String when you need to mutate the caller's string in place.

Same pattern for other types: &T to read; &mut T to mutate; T to take ownership.

Lifetimes (brief preview)

Every reference has a lifetime - how long it's valid. Most of the time the compiler infers them. Occasionally you see explicit lifetime annotations:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

'a is a lifetime parameter. It says: "the returned reference lives as long as both inputs." We're not going deeper here. Recognize the syntax ('a, &'a T); skim past in real code; learn properly when you actually need to write a lifetime annotation (which is rare - usually only when writing libraries).

Errors the borrow checker will give you

You will see these. They're not bugs; they're the compiler doing its job.

"borrow of moved value" - you used a value after moving it. Fix: clone (.clone()), or borrow instead of moving.

"cannot borrow as mutable because it is also borrowed as immutable" - you have an & reference live while trying to make &mut. Fix: scope the immutable borrow so it ends first.

"cannot borrow as mutable more than once at a time" - two &mut to the same value at the same time. Fix: scope one to end before the other starts.

"borrowed value does not live long enough" - you're trying to use a reference after the thing it points to is gone. Fix: ensure the owner outlives the reference.

When you see these, don't fight. Read the message. Often the compiler suggests the fix. Often the fix reveals a real bug in your thinking about the code.

The escape hatch: .clone()

When you really need to keep two owners, you can clone (deep copy):

let s1 = String::from("hello");
let s2 = s1.clone();
println!("{s1}, {s2}");   // both valid - two separate Strings

Clones cost - they allocate. Use sparingly. Borrowing (&) is almost always better.

Exercise

This page's exercise is more reading than coding. The point is to internalize, not invent.

  1. In a new file src/main.rs:
fn main() {
    let s = String::from("hello");
    takes_ownership(s);
    println!("{s}");    // does this compile? if not, why?
}

fn takes_ownership(s: String) {
    println!("{s}");
}

Predict whether it compiles. Run cargo check. Read any error. Now change takes_ownership(s) to takes_ownership(&s) and the function signature to fn takes_ownership(s: &String). Re-check.

  1. Try this:
fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s;
    println!("{r1}");
    println!("{r2}");
}

Predict. Run. Read the error.

  1. Now reorder:
fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    println!("{r1}");          // r1's last use
    let r2 = &mut s;
    r2.push_str(", world");
    println!("{r2}");
}

Predict. Run.

  1. Write a function fn add_exclamation(s: &mut String) that appends "!" to its argument. From main, create a mutable String, call the function, print the result.

What you might wonder

"This is a lot." It is. Take a break. Come back tomorrow and re-read.

"Why not just use a garbage collector like Go or Java?" GC is convenient but has costs: pause times (small but noticeable), memory overhead, and you can't run it in environments where GC is unacceptable (kernel modules, embedded, real-time). Rust gets memory safety without those costs.

"Won't I just clone everything to avoid the borrow checker?" You'll be tempted. Resist. The borrow checker is telling you something - often that your code has a structure problem (sharing where it shouldn't). Clone when the alternative is genuinely worse; don't clone to avoid thinking about ownership.

"Can I have shared mutable state at all?" Yes, via Rc<RefCell<T>> (single-threaded) or Arc<Mutex<T>> (multi-threaded). These "interior mutability" types let you sidestep the rules with runtime checks. We're not covering them here; recognize when you see them in real code.

"Will I really stop getting borrow-checker errors?" Mostly. After a few weeks, your mental model adjusts and you start writing code that compiles first try. Occasionally a tough case still trips you; that's normal even for experts.

Done

You can: - Explain ownership: each value has one owner; drops at end of scope. - Distinguish moves (non-Copy types) from copies (Copy types). - Use &T for immutable borrows and &mut T for mutable. - Recall the borrowing rules: one &mut XOR many &, no dangling. - Understand &str as a borrowed view, String as an owned value. - Recognize lifetime annotations. - Read borrow-checker errors without panic.

You've crossed the worst hill in the Rust learning curve. The rest of the path doesn't have a peak this steep.

Next: Collections →

Comments