On this page

References and borrowing: using without owning

15 min read TextCh. 2 — Ownership and Borrowing

References: using without owning

In the previous lesson we saw that passing a String to a function transfers its ownership. This is correct but verbose: you have to return the value to continue using it. References are the solution.

A reference allows you to refer to a value without taking its ownership. It is created with the & operator and is called "borrowing" — you are borrowing the value without owning it.

fn print_length(s: &String) -> usize {
    // s is a reference to String
    // We can read s, but we don't own it
    println!("The text is: {s}");
    s.len()
}

fn main() {
    let my_string = String::from("Rust is great");
    
    // We pass a reference — we don't move ownership
    let length = print_length(&my_string);
    
    // my_string is still valid here
    println!("'{my_string}' has {length} characters");
}

The &String parameter in the function indicates it receives a reference. When s goes out of scope at the end of the function, drop() is not called because the function does not have ownership.

Immutable references: &T

An immutable reference (&T) allows reading the value but not modifying it:

fn main() {
    let number = 42;
    let reference = &number;
    
    // Access through the reference (automatic dereferencing)
    println!("Number: {number}");
    println!("Through reference: {reference}");
    println!("Dereferenced: {}", *reference); // explicit *
    
    // Multiple immutable references at the same time: OK
    let s = String::from("text");
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    println!("{r1}, {r2}, {r3}"); // All valid simultaneously
    
    // Comparison: references are automatically dereferenced
    let a = String::from("hello");
    let b = String::from("hello");
    let ra = &a;
    let rb = &b;
    println!("Are they equal? {}", ra == rb); // true — compares content
}

Mutable references: &mut T

A mutable reference allows modifying the borrowed value:

fn capitalize_first(s: &mut String) {
    if let Some(first) = s.get_mut(0..1) {
        first.make_ascii_uppercase();
    }
}

fn add_punctuation(s: &mut String, punct: char) {
    s.push(punct);
}

fn main() {
    let mut message = String::from("hello rust");
    
    capitalize_first(&mut message);
    println!("{message}"); // "Hello rust"
    
    add_punctuation(&mut message, '!');
    println!("{message}"); // "Hello rust!"
}

The fundamental rule of the borrow checker

The borrow checker applies a central rule that prevents data races:

At any given time, you can have either:

  • One mutable reference (&mut T), OR
  • Any number of immutable references (&T)

But never both simultaneously.

fn main() {
    let mut s = String::from("hello");
    
    // Case 1: Multiple immutable references — OK
    let r1 = &s;
    let r2 = &s;
    println!("{r1}, {r2}"); // r1 and r2 used here — their lives end here
    
    // Case 2: One mutable reference — OK (r1 and r2 no longer in use)
    let rm = &mut s;
    rm.push_str(" world");
    println!("{rm}");
    
    // Case 3 (error): Mutable and immutable simultaneously
    // let r3 = &s;
    // let rm2 = &mut s; // Error: cannot borrow `s` as mutable
    //                   // because it is also borrowed as immutable
    // println!("{r3} {rm2}");
}

NLL: Non-Lexical Lifetimes

The compiler is smart: references have a "lifetime" that ends at the last point where they are used, not necessarily at the end of the block:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    println!("{r1} and {r2}"); // r1 and r2 end here (NLL)
    
    // Now we can create a mutable reference — r1 and r2 no longer exist
    let rm = &mut s;
    rm.push_str(" world");
    println!("{rm}");
}

This works thanks to Non-Lexical Lifetimes (NLL), introduced in Rust 2018. References don't live until the closing brace of the block, but until their last use.

Dangling references: the borrow checker saves you

In C/C++, it is possible to have a pointer pointing to memory that was already freed — a "dangling pointer." Rust makes this impossible at compile time:

// This code does NOT compile
fn create_dangling_reference() -> &String {
    let s = String::from("hello");
    &s // Error: s is dropped when this function exits
}  // s is freed here — the reference would point to invalid memory

The compiler rejects this code:

error[E0106]: missing lifetime specifier

The solution is to return the String directly (transferring ownership) rather than a reference:

fn create_string() -> String {
    let s = String::from("hello");
    s // Transfer ownership — not dangling
}

Slices: references to parts of collections

Slices are a special type of reference that point to a contiguous sequence of elements:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i]; // Slice of the first word
        }
    }
    
    &s[..] // If no space, the whole string is one word
}

fn main() {
    let sentence = String::from("hello world rust");
    
    // String slices (&str)
    let first = first_word(&sentence);
    println!("First word: {first}");
    
    // Array slices
    let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let half: &[i32] = &numbers[0..5];
    let rest: &[i32] = &numbers[5..];
    
    println!("Half: {:?}", half);
    println!("Rest: {:?}", rest);
    
    // sum(), min(), max() work on slices
    let sum: i32 = half.iter().sum();
    println!("Sum of first half: {sum}");
}

Summary of borrowing rules

Reference type Allows How many at once
&T Read Unlimited
&mut T Read and write Exactly 1 (and no &T)

These rules guarantee that:

  • There can be no data race conditions
  • There can be no dangling pointers
  • There can be no iterator invalidation

All verified at compile time, with no cost at runtime.


Now that you understand references and borrowing, the next lesson dives into lifetimes — the annotations the compiler uses to track how long references live.

References do not transfer ownership
A reference &T allows reading the value without becoming its owner. When the reference goes out of scope, the original value is NOT dropped — only the reference is discarded. The original owner retains ownership of the data.
Only one mutable reference at a time
The fundamental rule: you can have many immutable references (&T) OR exactly one mutable reference (&mut T), but never both at the same time. This rule prevents data races at compile time.
rust
fn calculate_length(s: &String) -> usize {
    s.len()  // Access without taking ownership
}

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

fn main() {
    let s1 = String::from("hello");

    // & creates a reference: we borrow s1 without moving it
    let length = calculate_length(&s1);
    println!("'{s1}' has {length} characters"); // s1 still valid

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

    // &mut: mutable reference — only ONE at a time
    append_world(&mut s2);
    println!("{s2}"); // "hello, world"

    // Multiple immutable references: OK
    let r1 = &s1;
    let r2 = &s1;
    println!("{r1} and {r2}"); // Both valid

    // Cannot have &mut while &T references exist
    // let r3 = &s2;
    // let r4 = &mut s2; // Error!
}