3. Ownership and moves
Full example: examples/l03_ownership.rs — cargo run --example l03_ownership.
The problem a GC solves
Section titled “The problem a GC solves”In C# and Java, objects live on the heap and many variables can point to the same object. Nobody “owns” it: the garbage collector frees it at some point after the last reference disappears.
That is convenient, but it costs a runtime, pauses, and memory headroom — and it only manages memory: files, sockets and locks still need using / IDisposable or try-with-resources.
Rust has no GC. Instead, the compiler enforces ownership rules and inserts the cleanup code itself, at compile time.
The three rules
Section titled “The three rules”- Each value has exactly one owner (a variable, a field, a collection element…).
- When the owner goes out of scope, the value is dropped (freed).
- Ownership can be moved to another owner; the previous owner can no longer be used.
let a = String::from("hello");let b = a; // ownership of the heap buffer moves to bprintln!("b = {b}"); // b = helloIn C#, var b = a; copies a reference: a and b now point to the same string, and both remain usable. In Rust, a is gone:
let a = String::from("hello");let b = a;println!("{a} {b}");error[E0382]: borrow of moved value: `a` --> e_move.rs:4:16 |2 | let a = String::from("hello"); | - move occurs because `a` has type `String`, which does not implement the `Copy` trait3 | let b = a; | - value moved here4 | println!("{a} {b}"); | ^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |3 | let b = a.clone(); | ++++++++Why? If both a and b owned the buffer, both would free it at the end of the scope — a double free. Moving makes “who frees this” unambiguous.
clone — an explicit deep copy
Section titled “clone — an explicit deep copy”let c = b.clone();println!("b = {b}, c = {c}"); // b = hello, c = helloclone() duplicates the heap data. It is always visible in the code, so expensive copies never happen by accident.
Copy types
Section titled “Copy types”Small values that live entirely on the stack are copied instead of moved:
let x = 5;let y = x;println!("x = {x}, y = {y}"); // x = 5, y = 5Integers, floats, bool, char, and tuples/arrays of those are Copy. This is close to C# value types (struct) — but in Rust your own structs are moved by default and only become Copy if you opt in with #[derive(Clone, Copy)].
| C# | Java | Rust | |
|---|---|---|---|
b = a with a heap object |
both reference the same object | both reference the same object | move: a unusable |
b = a with an int |
copy | copy | copy (Copy type) |
| explicit deep copy | ICloneable, copy constructor |
clone(), copy constructor |
.clone() |
Functions take ownership too
Section titled “Functions take ownership too”Passing a String by value moves it into the function:
fn take(s: String) -> usize { s.len()} // s is dropped here
let name = String::from("Ferris");let len = take(name);println!("{name} has {len} letters");error[E0382]: borrow of moved value: `name` --> e_move_fn.rs:8:16 |6 | let name = String::from("Ferris"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait7 | let len = take(name); | ---- value moved here8 | println!("{name} has {len} letters"); | ^^^^ value borrowed here after move |note: consider changing this parameter type in function `take` to borrow instead if owning the value isn't necessaryThe compiler already points at the real fix: borrow instead of taking ownership. That is the topic of lesson 4.
Returning a value moves ownership back out to the caller:
fn make_greeting(name: &str) -> String { format!("Hello, {name}!")}
let greeting = make_greeting("Ferris"); // greeting owns the new StringDrop — deterministic cleanup
Section titled “Drop — deterministic cleanup”When an owner goes out of scope, Rust calls drop, in reverse order of declaration. You can hook into it by implementing the Drop trait:
struct TempFile { name: String,}
impl Drop for TempFile { fn drop(&mut self) { println!("dropping {}", self.name); }}
fn main() { let _first = TempFile { name: "first.tmp".into() }; { let _inner = TempFile { name: "inner.tmp".into() }; println!("leaving inner scope"); } let _second = TempFile { name: "second.tmp".into() }; println!("end of main");}leaving inner scopedropping inner.tmpend of maindropping second.tmpdropping first.tmp| C# | Java | Rust | |
|---|---|---|---|
| Memory | GC, non-deterministic | GC, non-deterministic | freed at end of owner’s scope |
| Files, sockets, locks | using + IDisposable |
try-with-resources + AutoCloseable |
the same Drop, automatically |
| Forgetting to clean up | leak until finalizer (maybe) | leak until finalizer (maybe) | cleanup runs automatically; a leak needs an explicit std::mem::forget or a reference cycle (lesson 11) |
This pattern — acquire in a constructor, release in Drop — is how File, MutexGuard and network connections work in Rust. There is no using keyword because every scope already behaves like one.
Key takeaways
Section titled “Key takeaways”- One owner per value; the value is freed when the owner goes out of scope.
- Assigning or passing a non-
Copyvalue moves it; the old variable is unusable. .clone()is the explicit, visible deep copy.Dropgives deterministic cleanup for memory and resources, like an automaticusing.
Exercises
Section titled “Exercises”- Which lines compile? Explain each.
let a = 10;let b = a;println!("{a}"); // (1)
let s = String::from("x");let t = s;println!("{s}"); // (2)
let u = String::from("y");let v = u.clone();println!("{u} {v}"); // (3)Solution
- Compiles:
i32isCopy, sobgets a copy andastays usable. - Does not compile (
E0382):Stringis notCopy, soswas moved intot. - Compiles:
clone()creates an independentString, so both remain valid.
- Rewrite this Java method so the Rust version does not need
clone():
static int countVowels(String text) { /* … */ }// called as: countVowels(name); System.out.println(name);Solution
Take a borrowed string slice instead of an owned String, so the caller keeps ownership (explained in lesson 4):
fn count_vowels(text: &str) -> usize { text.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()}
let name = String::from("Ferris");let n = count_vowels(&name);println!("{name}: {n} vowels");- In what order are
a,bandcdropped?
let a = TempFile { name: "a".into() };let b = TempFile { name: "b".into() };let c = TempFile { name: "c".into() };drop(b);println!("done");Solution
b first (explicitly, via std::mem::drop, before done is printed), then at the end of the scope c, then a — reverse declaration order for the values still owned.