Skip to content

Borrowing

A reference borrows a value without owning it: many shared &T readers or one exclusive &mut T writer, never both, checked by the compiler.

Java has one kind of reference. Any code that has a reference can read the object, change it, or give the reference to another thread. The rules about who may change what are in your head, in comments, or in a lock.

The classic result is a ConcurrentModificationException. The loop reads the list through an iterator, and the body changes the same list:

Java

static void removeCancelled(List<Order> orders) {
for (Order order : orders) {
if (order.cancelled()) {
orders.remove(order); // ConcurrentModificationException at run time
}
}
}

Rust

for (i, order) in orders.iter().enumerate() { // shared borrow of `orders`...
if order.cancelled {
orders.remove(i); // ...so a mutable borrow is not allowed: E0502
}
}

In Java, this compiles and fails at run time (if you are lucky, the first time). In Rust, the same code does not compile. Error E0502 says: you cannot borrow orders as mutable, because it is also borrowed as immutable. The loop holds the shared borrow, and remove needs an exclusive one.

Rust has two kinds of reference:

Type Name How many at a time Can change the value
&T shared reference, shared borrow any number no
&mut T mutable reference, exclusive borrow exactly one, and no &T at the same time yes

There is no exact Java equivalent. The nearest idea: &T is a read-only view, and &mut T is a read-write view plus an exclusive lock. The difference is that the compiler checks the rules, and there is no cost at run time.

The fix for the loop above is the standard library method that does the iteration and the removal together. It is the same fix as in Java:

pub fn remove_cancelled(orders: &mut Vec<Order>) {
orders.retain(|order| !order.cancelled);
}

A borrow lasts from where it is made to where it is last used, not to the end of the block. This is called non-lexical lifetimes (NLL). It explains why code that looks like it breaks the rules compiles:

let first = &orders[0]; // shared borrow
let count = orders.len(); // another shared borrow: fine
println!("first is {}, {count} in total", first.id);
// `first` is not used after this line, so its borrow ends here
let last = orders.last_mut().expect("at least one order"); // mutable borrow
last.cancelled = true; // fine: no shared borrow is still in use
  1. Line 1: &orders[0] makes a shared borrow of orders. The name first holds it.
  2. Line 2: Another shared borrow. Any number of shared borrows can exist together.
  3. Line 3: The last use of first. Its borrow ends here, not at the end of the block.
  4. Line 6: last_mut() makes an exclusive borrow. This is allowed, because no shared borrow is still in use.
  5. Line 7: Change through the exclusive borrow. If line 3 came after this line, the code would not compile.

When you read Rust, look for the & and &mut:

  • In a parameter, &T means “I read it”, and &mut T means “I change it”. The caller keeps ownership in both cases.
  • On a method, &self reads the object, &mut self changes it, and self consumes it.
  • At a call site, f(&x) and f(&mut x) show you what the function may do to x.

The rule “one writer or many readers” is the rule that prevents data races. A data race needs two threads that access the same memory at the same time, where at least one of them writes. Rust’s borrow rules make that impossible in safe code, also across threads. This is why Rust can say “fearless concurrency”. See Threads and shared state.

The same rule also prevents single-threaded bugs: iterator invalidation (the loop above), a reference into a Vec that becomes invalid when the Vec grows, and “who changed my object” surprises.

Auto-deref and reborrowing, at a reading level

Section titled “Auto-deref and reborrowing, at a reading level”

You do not often see * in Rust code. The . operator follows references for you, so order.id works for an Order, a &Order and a &mut Order. You see * mainly in arithmetic and comparisons, where you need the value itself:

let id = order.id; // no `(*order).id`: field access dereferences for you
let cents: &u64 = &order.total_cents;
let doubled = *cents * 2; // arithmetic needs the value: `*` dereferences

A &mut T that you pass to a function is “reborrowed” for the call. The function gets a temporary exclusive borrow, and the original &mut is usable again after the call. This is why you can call two &mut self methods one after the other on the same value. You do not need to understand the details to review code. Read it as “lend it for the duration of the call”.

  • Parameters take the most general borrowed form. &str instead of &String, &[T] instead of &Vec<T>, &T instead of &Box<T>. Every owned form converts to these, so callers have more freedom. See S06: &String, &Vec<T>, &Box<T> parameters.
  • &self for methods that only read. &mut self only when the method changes the object. A &mut self getter blocks all other access while it is in use, for no reason.
  • Short borrows. Take a reference, use it, and let it end. Do not keep references in long-lived variables.
  • The borrow rules are followed, not avoided. When the compiler rejects a borrow, the good fix changes the structure: borrow later, split the work into two loops, or use a method like retain. A clone is sometimes correct, but it is not the default fix.
  • .clone() to make a borrow error go away. The most common AI fix for E0502 and E0499. Ask: what is the conflict, and does the copy hide a real bug? See S01: clone() to satisfy the borrow checker.
  • &mut self where &self is sufficient. It compiles, but it stops callers from holding any other reference while they call the method. Agents add mut when they are not sure.
  • &Vec<T>, &String and &Box<T> parameters. A direct translation of List<T> and String parameters. See S06: &String, &Vec<T>, &Box<T> parameters.
  • Indexes instead of references to avoid the borrow checker, for example storing usize positions in another struct and looking them up later. Sometimes this is correct (an arena), but often it hides a design that fights the rules.
  • RefCell or Rc<RefCell<T>> added only to get past a compile error. It moves the borrow check to run time, where a mistake is a panic. See Smart pointers.

The task: compute a cart total, and count the expensive prices.

  1. Worsetotal takes &mut self but only reads, and count_expensive takes &Vec<u64>.

    impl super::Cart {
    pub fn total(&mut self) -> u64 {
    self.prices.iter().sum()
    }
    }
    pub fn count_expensive(prices: &Vec<u64>, limit: u64) -> usize {
    prices.iter().filter(|&&p| p > limit).count()
    }
    Run in Playground (opens the Rust Playground in a new tab)
  2. Better&self for a read-only method, and a slice parameter that accepts any list of prices.

    impl super::Cart {
    pub fn total_cents(&self) -> u64 {
    self.prices.iter().sum()
    }
    }
    pub fn count_expensive(prices: &[u64], limit: u64) -> usize {
    prices.iter().filter(|&&p| p > limit).count()
    }
    Run in Playground (opens the Rust Playground in a new tab)

Both versions compile and give the same result. The “worse” version has two costs. A caller that holds &cart cannot call total(), so the caller must restructure its code or clone the cart. And count_expensive(&prices[..10], limit) does not compile with the &Vec version, so a caller with part of a list must copy it into a new Vec.

&mut self is correct when the method changes the object, or caches a result in it. If a “read” method needs &mut self to update a cache, ask if the cache belongs in the type.

  1. A loop iterates over orders and calls orders.push(...) in its body. What happens?

    Show the answer

    It does not compile (E0502 or E0499). The loop holds a borrow of orders for its whole duration. Collect the new orders in a second Vec and append them after the loop, or use a method that does both steps.

  2. A method fn total(&mut self) -> u64 only adds up numbers. Why is this a problem?

    Show the answer

    It needs an exclusive borrow, so no other reference to the object can be in use when you call it. Code that holds &cart cannot call it at all. Change it to &self.

  3. Code takes let first = &v[0];, then calls v.push(1);, then uses first. Why does this not compile, and what bug does that prevent?

    Show the answer

    push needs &mut v while the shared borrow first is still in use. If the Vec grows, it can move its elements to a new buffer, and first would point to freed memory. In Java the garbage collector prevents this, but the same pattern causes ConcurrentModificationException with iterators.

  4. When is it correct to clone to solve a borrow error?

    Show the answer

    When you need an independent copy anyway: the copy goes to another thread, into a struct that outlives the original, or the original changes and you need the old value. If the copy is only read and then thrown away, borrow instead.