Ownership and moves
In one sentence
Section titled “In one sentence”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.
The Java you know
Section titled “The Java you know”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 objectarchived.lines.add(300L); // the change is visible through `order` tooSystem.out.println(order.lines.size()); // 2This 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);}The Rust you will see
Section titled “The Rust you will see”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 objectarchived.lines.add(300L); // the change is visible through `order` tooSystem.out.println(order.lines.size()); // 2Rust
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 movelet 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:
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).
How to read it
Section titled “How to read it”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);}- Line 2:
order: Order(no&) takes ownership. The caller gives the order away. Whenshipreturns, the order is dropped, unlessshipreturns it or stores it. - Line 7:
order: &Orderborrows the order to read it. The caller keeps it. This is the usual parameter type. - Line 12:
order: &mut Orderborrows it to change it. The caller keeps it, with the change. Only one&mutcan 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 changelet total = total_cents(&order); // lend it to readlet shipped_id = ship(order); // give it away; `order` is goneWhen 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.
Drop: cleanup at the end of the scope
Section titled “Drop: cleanup at the end of the scope”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.
Copy types: copied, not moved
Section titled “Copy types: copied, not moved”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.
Stack and heap, at a reading level
Section titled “Stack and heap, at a reading level”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>),}What good looks like
Section titled “What good looks like”- Parameters borrow by default. Most functions take
&T,&stror&[T]. A function takesTby 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 noclose()method that callers must remember. - Consuming methods for state changes.
fn commit(self)orfn send(self)makes it impossible to use the old state again.
Red flags in AI output
Section titled “Red flags in AI output”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) -> Orderthat 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 plainOrderworks. Java developers expect objects to be on the heap. Rust does not need that.- A manual
close()ordispose()method that callers must remember to call, instead ofDrop.
- S01Severity: Medium
clone()to satisfy the borrow checkerA copy of the data is made only to make a borrow error go away. - S03Severity: Medium
Rc<RefCell<T>>object graph (Java translated directly)A Java object graph copied with shared, mutable, reference-counted cells. - S05Severity: LowOwned parameters (
String,Vec<T>) where a borrow is sufficientThe function takes ownership of data that it only reads.
Better vs worse
Section titled “Better vs worse”The task: add a shipping line to an order during checkout.
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) 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.
Check yourself
Section titled “Check yourself”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.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
&Tand remove the clone. If it really keeps the value, the clone can be correct, but check if the caller still needs its copy.Where is the
close()call for a file in Rust?Show the answer
There is none.
FileimplementsDrop, so the file closes when its owner goes out of scope. Look instead for values that stay in scope longer than necessary.let b = a;and thenais used again. When does this compile?Show the answer
When the type of
aisCopy(for exampleu64,bool, or a small struct that derivesCopy). For other types,amoved tob, and the compiler rejects the use with E0382.