Rust / Basic / Lesson 3
Ownership, moves, and functions
Track who owns each value as variables and function calls transfer it, and choose copying or cloning only when the required semantics justify it.
On this page
Lesson goals
- Explain scope-based destruction and ownership transfer.
- Distinguish Copy, Clone, and a move.
- Read function signatures as ownership contracts.
An owner is responsible for a value
Ownership connects a value to responsibility for using and eventually dropping it. A String manages an owned UTF-8 buffer, and dropping its owner normally releases that allocation. Scope makes cleanup predictable without requiring a tracing garbage collector. Not every value allocates, and ownership is not simply a rule that all data lives on the heap. A local integer is also a value with a scope. Rust checks initialization and ownership statically, while resource exhaustion, panics, and incorrect application rules remain possible in safe programs.
A value parameter receives ownership
Passing a String to a String parameter transfers ownership unless a separate copy is explicitly created. The caller cannot subsequently use that moved binding as though it still owned the same value. The first program deliberately uses text only as the function argument. The callee reads its length and drops its owned String when the function returns. The returned usize is an independent count. A move is a semantic transfer; do not infer a fixed number of machine instructions or a guaranteed memory address from the source-level operation.
fn consume(text: String) -> usize { text.len() }
fn main() {
let text = String::from("map");
let count = consume(text);
println!("{count}");
}
3
consume owns the three-byte string during the call. main prints only the returned count and never reuses the moved String.
Independent values require the right operation
Copy types can be implicitly duplicated instead of invalidating the old binding; ordinary fixed-width integers are examples. Copy requires Clone and is unsuitable for values whose duplication would violate ownership or destruction semantics. Clone is an explicit operation with type-defined behavior and potentially significant cost. Cloning a String creates independent owned text; cloning an Rc or Arc instead shares the same allocation through another owner. The second example relies specifically on String cloning, then changes the cloned string. Never describe all Clone implementations as deep copies of every reachable object.
fn main() {
let original = String::from("sun");
let mut copy = original.clone();
copy.push('!');
println!("{original} {copy}");
}
sun sun!
Appending to copy leaves original unchanged. Both owners drop their own text when main finishes.
Return newly created data by value
A function that creates a new String should usually return String, allowing the caller to own the result. Returning a reference to a local owner would let the reference escape storage that has already been dropped, so safe Rust rejects it. Taking ownership is appropriate when a function stores, transforms, or consumes its input. Borrowing is preferable when it only needs temporary access, as the next lesson explains. Choose the interface from the data flow rather than repeatedly cloning inputs to silence ownership errors. Compiler diagnostics often expose an unclear design decision.
Review values at every boundary
Moving an owned field out of an ordinary aggregate can partially move the aggregate, restricting later use of the whole value while other unmoved fields may remain accessible. Types implementing Drop have additional restrictions on moving fields because destruction needs a coherent value. Keep introductory designs simple with whole-value moves and explicit destructuring. For every call, ask whether the argument is copied, moved, or borrowed and whether a result establishes new ownership. Test the desired state transitions, not only printed text, and do not assume a memory leak is impossible merely because code is safe.
Common errors and fixes
E0382 reports use of a moved String.
- Why it happens
- A value parameter or assignment took ownership.
- How to fix it
- Borrow, transfer deliberately, or clone when independence is required.
- Prevent it next time
- Read signatures before choosing argument syntax.
Cloning a shared handle does not isolate mutations.
- Why it happens
- Its Clone implementation shares ownership of the same data.
- How to fix it
- Clone the underlying value when a snapshot is required.
- Prevent it next time
- Check Clone semantics for each type.
A local reference cannot be returned.
- Why it happens
- The local owner ends before caller access.
- How to fix it
- Return an owned result instead.
- Prevent it next time
- Draw owner scopes when data crosses function boundaries.
Check your understanding
1. A function only prints a String, but its caller needs the text afterward. Choose between consuming, borrowing, and cloning and explain your preferred contract.
Show worked solution
Prefer a borrowed &str parameter because the function only observes text during its call. The caller keeps ownership and can use the String afterward. Cloning would work but allocate unnecessary independent text. Consuming is appropriate only if the function genuinely needs ownership, such as retaining the string beyond the call.
2. Extend the clone example by assigning an integer seven to another binding and then printing both integers. Contrast that behavior with assigning original to another String binding.
Show worked solution
A u32 value is Copy, so both integer bindings remain usable and contain seven. Assigning original to another String binding moves ownership; using original afterward produces E0382 unless a separate clone was created. This difference follows from the types, not from whether the local binding was declared mutable.
Key takeaways
- Follow ownership through assignments and calls.
- Use cloning for a required semantic result.
- Prefer owned returns for newly constructed data.