Skip to content

Ownership and moves

Each value has exactly one owner; assignment and function calls move the value to a new owner, and the value is freed when its owner goes out of scope.

In Java, every object lives on the heap. A variable holds a reference, and any number of references can point to the same object. The garbage collector frees the object some time after the last reference is gone.

var order = new Order(7);
order.lines.add(1_200L);
var archived = archive(order); // a second reference to the same object
archived.lines.add(300L); // the change is visible through `order` too
System.out.println(order.lines.size()); // 2

This is so normal in Java that you do not see it. Two names, one object: a change through archived is visible through order. Nothing in the code says who is responsible for the object, and nothing needs to, because the garbage collector cleans up.

A method signature in Java also says nothing about responsibility. All three of these methods receive “a reference to an order”. You must read the body, or the Javadoc, to know which one keeps it or changes it:

// The signatures do not say who keeps the order, or who may change it.
static long ship(Order order) {
return order.id;
}
static long totalCents(Order order) {
return order.lines.stream().mapToLong(Long::longValue).sum();
}
static void addLine(Order order, long cents) {
order.lines.add(cents);
}

Rust has no garbage collector. Instead, the compiler tracks one owner for each value. When the owner goes out of scope, Rust frees the value at that point, every time.

Java

var order = new Order(7);
order.lines.add(1_200L);
var archived = archive(order); // a second reference to the same object
archived.lines.add(300L); // the change is visible through `order` too
System.out.println(order.lines.size()); // 2

Rust

let order = Order {
id: 7,
lines: vec![1_200, 300],
};
let archived = archive(order); // `order` moves into `archive`
// println!("{}", order.id); // would not compile: E0382, use after move

let archived = archive(order); does not copy a reference. It moves the order into archive. After the move, the name order is no longer usable. The compiler rejects any later use:

Does not compile: E0382
let order = Order { id: 7, lines: vec![1_200, 300] };
let archived = archive(order); // `order` moves into `archive`
println!("{}", order.id); // error E0382: borrow of moved value: `order`

This is the most common compiler error in Rust: E0382, “borrow of moved value”. You will see agents fix it. How they fix it is one of the most important things to review (see “Red flags” below).

The function signature tells you what happens to an argument. This is the most useful reading skill in Rust, and Java has nothing like it.

/// Takes ownership. The caller cannot use `order` after the call.
pub fn ship(order: Order) -> u64 {
order.id
}
/// Borrows to read. The caller keeps `order`.
pub fn total_cents(order: &Order) -> u64 {
order.lines.iter().sum()
}
/// Borrows to change. The caller keeps `order`, with the change.
pub fn add_line(order: &mut Order, cents: u64) {
order.lines.push(cents);
}
  1. Line 2: order: Order (no &) takes ownership. The caller gives the order away. When ship returns, the order is dropped, unless ship returns it or stores it.
  2. Line 7: order: &Order borrows the order to read it. The caller keeps it. This is the usual parameter type.
  3. Line 12: order: &mut Order borrows it to change it. The caller keeps it, with the change. Only one &mut can exist at a time.

The call site shows the same three cases. The & and &mut in the call are visible, so you can see at the call what the function may do with your value:

let mut order = Order {
id: 7,
lines: vec![1_200],
};
add_line(&mut order, 300); // lend it to change
let total = total_cents(&order); // lend it to read
let shipped_id = ship(order); // give it away; `order` is gone

When you review a function, read its parameters first:

Parameter Meaning Java analogy
x: T Takes ownership. The caller cannot use x after the call. No equivalent. Closest: “the method keeps the object, do not touch it again”.
x: &T Shared borrow. Read only. A reference to an object that the method promises not to change.
x: &mut T Exclusive borrow. Can change x. A reference, plus a lock that no one else holds.

self, &self and &mut self on methods follow the same rules. See Borrowing for the borrow rules.

When the owner goes out of scope, Rust calls drop on the value. For most types this frees memory. A type can implement the Drop trait to do more, for example to roll back a transaction. This is the RAII pattern (“resource acquisition is initialization”).

Compare it to try-with-resources in Java, or using with IDisposable in C#:

Java

static final class Transaction implements AutoCloseable {
private final long id;
private boolean committed;
Transaction(long id) {
this.id = id;
System.out.println("begin " + id);
}
void commit() {
committed = true;
System.out.println("commit " + id);
}
@Override
public void close() {
if (!committed) {
System.out.println("rollback " + id);
}
}
}
static void save(boolean valid) {
try (var tx = new Transaction(42)) {
if (!valid) {
throw new IllegalArgumentException("validation failed"); // close() runs: rollback
}
tx.commit();
}
}

Rust

pub struct Transaction {
id: u64,
committed: bool,
}
impl Transaction {
pub fn begin(id: u64) -> Self {
log(format!("begin {id}"));
Self {
id,
committed: false,
}
}
pub fn commit(mut self) {
self.committed = true;
log(format!("commit {}", self.id));
} // `self` is dropped here
}
impl Drop for Transaction {
fn drop(&mut self) {
if !self.committed {
log(format!("rollback {}", self.id));
}
}
}

The difference: in Rust you do not need a try block. Every value has a scope, so every value is “auto-closeable”. commit(mut self) takes self by value. After tx.commit(), the transaction is consumed: the compiler does not let you use it again, so you cannot commit twice.

let tx = Transaction::begin(42);
if fail {
return Err(ValidationFailed); // `tx` is dropped here: rollback
}
tx.commit();
Ok(())

The early return drops tx, and drop rolls back. Files, locks, sockets, and database connections in Rust all close this way. When you review Rust, you do not look for missing close() calls. You look for values that live longer than necessary, for example a lock guard that is held across slow work.

Small types that are only bits, with no heap data, can be Copy. Integers, bool, char, floats, and shared references are Copy. A struct can derive Copy if all its fields are Copy. Assignment copies them, and the old name stays usable:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrderId(pub u64);
pub fn demo() -> (OrderId, OrderId) {
let first = OrderId(7);
let second = first; // a copy, not a move: `OrderId` is `Copy`
(first, second) // both are still usable
}

This is like Java primitives, or a C# struct. String and Vec are never Copy, because they own heap memory. See Clone and Copy.

Simplified: a Rust value lives on the stack unless a type puts it on the heap. String, Vec<T> and Box<T> own heap memory. A move copies only the small stack part (for a Vec: a pointer, a length and a capacity). It does not copy the elements. So a move is cheap, even for a large Vec.

Box<T> puts one value on the heap and owns it. You see it for recursive types, which otherwise have no fixed size, and for trait objects (Box<dyn Trait>):

/// A category tree. `Box` puts each child on the heap, so the type has a fixed size.
pub enum Category {
Leaf(String),
Group(String, Vec<Category>),
Alias(String, Box<Category>),
}
  • Parameters borrow by default. Most functions take &T, &str or &[T]. A function takes T by value only when it keeps the value (stores it in a struct, sends it to another thread) or consumes it (commit(self)).
  • Ownership follows the data flow. A constructor takes owned values because the struct keeps them. A report function borrows because it only reads.
  • Few clones. Each .clone() has a reason that you can say in one sentence. See Clone and Copy.
  • Cleanup is in Drop, not in comments. Resource types close themselves. There is no close() method that callers must remember.
  • Consuming methods for state changes. fn commit(self) or fn send(self) makes it impossible to use the old state again.

Agents often write Java with Rust syntax, and then fix the compiler errors one at a time. The fixes are where the problems are.

  • A .clone() that was added to fix E0382. The agent gets “use of moved value”, adds .clone() before the move, and the error goes away. Often the correct fix is to borrow. See S01: clone() to satisfy the borrow checker.
  • Owned parameters for read-only functions. fn greeting(name: String) forces every caller to give away or clone a string. See S05: Owned parameters (String, Vec<T>) where a borrow is sufficient.
  • Take, change, return. fn add(x: Order) -> Order that changes the value and gives it back. This is a Java-style “return the modified object” habit. Use &mut Order.
  • Box<T> for no reason. Box<Order> as a field or a parameter, where a plain Order works. Java developers expect objects to be on the heap. Rust does not need that.
  • A manual close() or dispose() method that callers must remember to call, instead of Drop.

The task: add a shipping line to an order during checkout.

  1. WorseTakes the order by value, so the caller clones it. The original is not changed: the caller must replace it with the return value.

    pub fn add_shipping(order: Order) -> Order {
    let mut order = order;
    order.lines.push(495);
    order
    }
    pub fn checkout(order: &Order) -> Order {
    add_shipping(order.clone()) // copy, change, return the copy
    }
    Run in Playground (opens the Rust Playground in a new tab)
  2. BetterBorrows the order mutably. No copy, and the signature says "this changes your order".

    pub fn add_shipping(order: &mut Order) {
    order.lines.push(495);
    }
    pub fn checkout(order: &mut Order) {
    add_shipping(order);
    }
    Run in Playground (opens the Rust Playground in a new tab)

The “worse” version compiles and works. It is a translation of a Java method that returns the modified object. It has two costs. First, the clone() copies the whole order, with all its lines. Second, the caller now has two orders, and must know which one is current. The “better” version has one order, and &mut says in the signature that it changes.

The “worse” pattern is correct when the function really builds a new value, and the old one is not needed. In that case, take self or T by value and return the new value, without a clone: fn with_shipping(mut self) -> Self. The builder pattern uses this.

  1. A function has the signature fn store(order: Order). What can the caller do with its order after the call?

    Show the answer

    Nothing. The order moved into store. If the caller needs it again, either the function must borrow (&Order) or the caller must clone before the call. Ask why the function takes ownership. If it stores the order, the signature is correct.

  2. An agent fixed error E0382 by adding .clone(). What do you check?

    Show the answer

    Whether the function that took ownership needs it. If it only reads the value, change its parameter to &T and remove the clone. If it really keeps the value, the clone can be correct, but check if the caller still needs its copy.

  3. Where is the close() call for a file in Rust?

    Show the answer

    There is none. File implements Drop, so the file closes when its owner goes out of scope. Look instead for values that stay in scope longer than necessary.

  4. let b = a; and then a is used again. When does this compile?

    Show the answer

    When the type of a is Copy (for example u64, bool, or a small struct that derives Copy). For other types, a moved to b, and the compiler rejects the use with E0382.