References in Rust: &T, &mut T & Deref Coercion

Max WellsMax WellsFounder of Rustify

TL;DR: A reference in Rust is a pointer that borrows a value without taking ownership. &T is a shared (immutable) reference, many can exist at once. &mut T is a mutable reference, only one can exist at a time, and no shared references can coexist with it. These rules are enforced at compile time by the borrow checker. References must not outlive the value they point to, this is enforced by lifetimes. References are Rust's primary mechanism for passing values to functions without copying or moving them.


What Is a Reference?

A reference is a pointer to a value that does not take ownership, the original owner keeps the value, and the reference is valid only for a limited scope.

fn main() {
    let s = String::from("hello");
 
    let r = &s;           // borrow s; s is still the owner
    println!("{r}");      // use the reference
    println!("{s}");      // s is still valid; we only borrowed it
 
    // Without references; ownership moves:
    // let r = s;         // s moved into r; s is no longer valid
}

What Is the Difference Between &T and &mut T?

&T allows reading, &mut T allows both reading and writing. The borrow checker enforces that they never coexist.

fn main() {
    let mut value = 42;
 
    // Shared reference; can have many at once
    let r1 = &value;
    let r2 = &value;
    println!("{r1} {r2}");  // ok; two shared references
 
    // Mutable reference; only one, and no shared refs at the same time
    let r3 = &mut value;
    *r3 += 1;               // dereference to modify
    println!("{r3}");       // 43
 
    // This would fail; can't have r1 and r3 active simultaneously:
    // println!("{r1} {r3}"); // ❌ borrow checker error
}

The rules:

  • Any number of &T at the same time ✅
  • Exactly one &mut T, and no &T at the same time ✅
  • &T and &mut T together ❌

How Do References Work in Function Parameters?

Pass &T to let a function read a value, &mut T to let it modify it, without giving up ownership.

// Takes a shared reference; caller keeps ownership
fn print_length(s: &String) {
    println!("length: {}", s.len());
}
 
// Takes a mutable reference; can modify the caller's value
fn make_uppercase(s: &mut String) {
    s.make_ascii_uppercase();
}
 
fn main() {
    let mut text = String::from("hello");
 
    print_length(&text);        // borrow: &String
    make_uppercase(&mut text);  // mutable borrow: &mut String
    println!("{text}");         // "HELLO"; modified in place
}

What Is Automatic Dereferencing?

Rust automatically inserts * dereferences when calling methods, you rarely need to dereference manually.

fn main() {
    let s = String::from("hello world");
    let r = &s;
 
    // Both work; Rust auto-derefs the reference
    println!("{}", r.len());   // equivalent to (*r).len()
    println!("{}", r.contains("world"));
 
    // Explicit deref when modifying:
    let mut n = 42;
    let r = &mut n;
    *r += 1;           // must deref to assign
    println!("{}", r); // auto-deref for Display
}

How Do References Relate to Lifetimes?

A reference must not outlive the value it points to, the borrow checker enforces this via lifetimes.

// This fails; r would outlive the value it references
fn dangling() -> &String {    // ❌ missing lifetime specifier
    let s = String::from("hello");
    &s  // s is dropped here; returning a dangling reference
}
 
// Fix 1: return owned value
fn owned() -> String {
    String::from("hello")
}
 
// Fix 2: take a reference and return a reference tied to the input
fn first_word(s: &str) -> &str {  // lifetime elision makes this work
    &s[..s.find(' ').unwrap_or(s.len())]
}

What Is Deref Coercion?

Rust automatically converts &String to &str, &Vec<T> to &[T], and &Box<T> to &T, you can pass them interchangeably.

fn greet(name: &str) {
    println!("Hello, {name}!");
}
 
fn main() {
    let owned = String::from("Alice");
    greet(&owned);          // &String → &str (deref coercion)
 
    let boxed = Box::new("Bob");
    greet(&boxed);          // &Box<&str> → &&str → &str
 
    let v = vec!['a', 'b'];
    let slice: &[char] = &v; // &Vec<char> → &[char]
}

Frequently Asked Questions

A raw pointer (*const T, *mut T) has no safety guarantees, it can be null, dangling, or aliased. A reference (&T, &mut T) is guaranteed by the compiler to be non-null, valid, and correctly aliased. References are safe; raw pointers require unsafe.

Two &mut T to the same data would allow data races, two simultaneous writers. Rust prevents this at compile time. The single &mut T rule guarantees that mutations are always exclusive.

Moving transfers ownership, the original variable becomes invalid. Borrowing (& / &mut) creates a reference, the original variable remains valid after the borrow ends. Pass by reference when you don't need to own the value.

NLL (stable since Rust 2018) means the borrow ends at the last use, not at the end of the block. This allows code that was previously rejected to compile:

let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}");  // last use of `first`
v.push(4);            // now ok; borrow ended above

Sources


  • Ownership: References are Rust's borrowing mechanism
  • Lifetime: Lifetimes govern how long references are valid
  • Borrow Checker: Enforces reference rules at compile time
  • Slice: Slices are always used via references (&[T])

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?