Phase 2Ownership & borrowing

#6 References and borrowing

&, &mut, borrowing rules

What is borrowing?

In the previous lesson, we saw that passing a value to a function moves ownership. Borrowing lets you use a value without taking ownership of it. You create a reference to the value instead:

Borrowing with &
fn calculate_length(s: &String) -> usize {
    s.len()
}   // s goes out of scope, but it doesn't own the data — nothing is dropped

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s);  // Pass a reference
    println!("Length of \"{s}\" is {len}");  // s is still valid!
}

The & symbol creates a reference. The function borrows the value but does not own it. When the reference goes out of scope, the original value is unaffected.

Immutable references (&T)

By default, references are immutable. You can read the value but cannot modify it:

Immutable reference
fn print_greeting(s: &String) {
    println!("Hello, {s}!");
    // s.push_str("!!!");  // ERROR: cannot modify through immutable reference
}

fn main() {
    let name = String::from("Rust");
    print_greeting(&name);
    println!("s is still valid: {name}");
}

You can have multiple immutable references at the same time — reading is always safe when no one is writing.

Mutable references (&mut T)

To modify a borrowed value, you need a mutable reference with &mut:

Mutable reference
fn add_world(s: &mut String) {
    s.push_str(", world!");
}

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

Notice that both the variable (let mut s) and the reference (&mut s) must be declared as mutable.

Borrowing rules

Rust enforces two critical rules at compile time to prevent data races:

The two borrowing rules
// Rule 1: You can have EITHER:
//   - Any number of immutable references (&T)
//   - OR exactly one mutable reference (&mut T)
// ...but NOT both at the same time.

let mut s = String::from("hello");

let r1 = &s;      // OK — immutable borrow
let r2 = &s;      // OK — another immutable borrow
println!("{r1}, {r2}");
// r1 and r2 are no longer used after this point

let r3 = &mut s;  // OK — mutable borrow (r1, r2 are done)
r3.push_str("!");

// Rule 2: References must always be valid
// (no dangling references — see below)

Rust's borrow checker is smart: a reference's lifetime ends at its last use, not at the end of the scope. This is called Non-Lexical Lifetimes (NLL).

Dangling references

Rust prevents dangling references — references that point to freed memory. The compiler catches this at compile time:

Dangling reference (does NOT compile)
fn dangle() -> &String {
    let s = String::from("hello");
    &s  // ERROR: s is dropped at end of function!
}       // s goes out of scope — reference would be invalid

// Fix: return the owned value instead
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // Ownership is moved to the caller
}

This guarantee is one of Rust's biggest strengths: if your code compiles, there are no dangling pointers or use-after-free bugs.

Your turn

Try running the references demo with cargo run and cargo check in the terminal below:

terminal — cargo
user@stemlegacy:~/references-demo$