Apps Artificial Intelligence CSS DevOps Go JavaScript Laravel Linux MongoDB MySQL PHP Python Rust Svelte Vue

Understanding References and Borrowing in Rust

3 min read .
Understanding References and Borrowing in Rust

References and borrowing let Rust code access data without taking ownership of it. These concepts are fundamental to Rust’s memory-safety model and allow programs to share and mutate data under rules that the compiler can verify.

What Is a Reference in Rust?

A reference points to a value owned elsewhere without becoming responsible for dropping that value. References use & syntax and are either immutable or mutable:

  • Immutable reference (&T): allows read-only access.
  • Mutable reference (&mut T): allows mutation while enforcing exclusive access rules.

Immutable References

Immutable references let a function inspect a value without consuming it:

fn main() {
    let s = String::from("Rust");
    let len = calculate_length(&s);
    println!("The length of '{}' is {}.", s, len);
}

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

Key points:

  • &s borrows s immutably.
  • calculate_length can read the string but cannot modify it.
  • Ownership remains with s, so it can still be used after the function call.

For APIs that only need string data, &str is often more general than &String:

fn calculate_length(s: &str) -> usize {
    s.len()
}

Mutable References

A mutable reference lets borrowed data be changed:

fn main() {
    let mut s = String::from("Hello");
    change(&mut s);
    println!("{}", s);
}

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

Here &mut s grants temporary mutable access to the function.

Borrowing Rules

Rust enforces rules that prevent conflicting access:

  1. You can have multiple immutable references when there is no overlapping mutable reference.
  2. You can have a mutable reference only when no other conflicting references are active.
  3. A reference may never outlive the value it refers to.

These rules are based on the actual lifetimes of references, not simply the braces around a block. With non-lexical lifetimes, a borrow can end after its final use.

Multiple Immutable References

Read-only access can be shared:

fn main() {
    let s = String::from("Rust");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
}

Because neither reference can mutate s, there is no write conflict.

Exclusive Mutable Access

Overlapping mutable references are rejected:

fn main() {
    let mut s = String::from("Rust");

    let r1 = &mut s;
    // let r2 = &mut s; // error if r1 is still active
    println!("{}", r1);
}

This restriction is one of the mechanisms Rust uses to prevent data races.

References Must Stay Valid

Rust does not allow a reference to outlive the value it points to:

fn main() {
    let r;

    {
        let x = 5;
        // r = &x; // would be invalid outside this block
    }

    // println!("{}", r);
}

The compiler rejects code that could create a dangling reference.

Slices Borrow Part of a Collection

A slice is a reference to a contiguous portion of data:

fn main() {
    let s = String::from("Hello, world!");
    let hello = &s[0..5];
    let world = &s[7..12];

    println!("{} {}", hello, world);
}

String slicing uses byte indexes, so take care with UTF-8 text: indexes must fall on valid character boundaries or the operation will panic.

Common Borrowing Errors

  1. Conflicting references: code tries to mutate a value while another active borrow requires incompatible access. Fix: shorten the borrow, reorder operations, or restructure data so the accesses do not overlap.

  2. Dangling-reference attempts: a reference would live longer than the referenced data. Fix: return owned data or ensure the referenced value has a sufficiently long lifetime.

  3. Borrow-checker errors around collections: a collection is borrowed while code also tries to mutate it. Fix: separate lookup and mutation phases, use APIs designed for entry-based mutation, or change the data layout.

Best Practices

  1. Prefer immutable borrowing when mutation is not required.
  2. Keep mutable borrows as narrow as practical.
  3. Accept slices such as &str and &[T] when a function does not require ownership or a specific container type.
  4. Treat borrow-checker diagnostics as information about conflicting ownership assumptions rather than bypassing them with unnecessary clones.

Conclusion

References and borrowing allow Rust programs to share data efficiently while preserving strong memory-safety guarantees. Once you understand when a borrow begins and ends, and why mutable access must be exclusive, many Rust design patterns become much more intuitive.

Related Posts

chevron-up