Skip to content
AgileLogics
7 min read

References & Borrowing

Prerequisites: Ownership

Moving values into functions and handing them back gets tedious. Instead, you can borrow a value with a reference: a pointer that lets code use a value without taking ownership of it.

Syntax

let s = String::from("hi");
let r = &s;          // shared (immutable) reference

let mut t = String::from("hi");
let m = &mut t;      // mutable reference
m.push('!');
  • &value creates a shared reference. You can read through it, but not change the value.
  • &mut value creates a mutable reference. You can change the value through it. The variable itself must be declared mut.
  • A function parameter of type &String or &mut String borrows the argument. When the function returns, the borrow ends and the caller still owns the value.

The borrowing rules

At any given time, you can have either:

  • one mutable reference, or
  • any number of shared references,

but not both. And references must always be valid — a reference can never outlive the value it points to.

These rules prevent data races and bugs where data changes while someone else is reading it. A borrow lasts from where it's created to where it's last used, not necessarily to the end of the scope — so once you've finished with shared references, you can create a mutable one.

Example

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s); // borrow `s`
    println!("'{s}' has length {len}"); // `s` is still ours

    let mut greeting = String::from("Hello");
    add_world(&mut greeting);
    println!("{greeting}");

    let r1 = &greeting;
    let r2 = &greeting; // many shared references: fine
    println!("{r1} / {r2}");
    // r1 and r2 aren't used after this line, so their borrows end

    let r3 = &mut greeting; // now a mutable reference is allowed
    r3.push('!');
    println!("{r3}");
}

fn calculate_length(text: &String) -> usize {
    text.len()
}

fn add_world(text: &mut String) {
    text.push_str(", world");
}

(&String parameters keep this example simple. The next lesson shows why &str is the more flexible choice for text parameters.)

Expected output

'hello' has length 5
Hello, world
Hello, world / Hello, world
Hello, world!

Common mistakes

  • Mixing a shared and a mutable borrow:
let mut s = String::from("hi");
let r1 = &s;
let r2 = &mut s;
r2.push('!');
println!("{r1}");
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

r1 is still used on the last line, so its borrow is still active when r2 is created. Finish using r1 first.

  • Two mutable borrows at once:
error[E0499]: cannot borrow `s` as mutable more than once at a time
  • Returning a reference to a local variable. The local is dropped when the function ends, so the reference would dangle:
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}
error[E0106]: missing lifetime specifier

Return the owned String instead.

Practice

Write a function shout that takes a &mut String and appends "!!!" to it. Call it on a String holding "hey", then print the result: hey!!!.

Show solution
fn shout(text: &mut String) {
    text.push_str("!!!");
}

fn main() {
    let mut message = String::from("hey");
    shout(&mut message);
    println!("{message}");
}