Rust · lesson 8 of 30
Beginner
15- 1 Introduction to Rust
- 2 Setting Up Rust
- 3 Variables in Rust
- 4 Data Types in Rust
- 5 Operators in Rust
- 6 Control Flow in Rust
- 7 Functions in Rust
- 8 Ownership in Rust
- 9 Borrowing and References
- 11 Structs in Rust
- 12 Enums in Rust
- 13 Pattern Matching in Rust
- 17 Collections in Rust
- 25 Testing in Rust
- 26 File I/O in Rust
Intermediate
9Advanced
6Ownership in Rust
Understand Rust's ownership model through runnable examples: the three rules, move semantics, the Copy trait, cloning, and Drop/RAII order.
By SkillByExample · Updated
The Three Ownership Rules
Ownership is the feature that makes Rust unique. It is the mechanism through which Rust achieves memory safety without a garbage collector — no reference counting, no GC pauses, no runtime overhead. The entire system is enforced at compile time through three rules:
- Each value in Rust has exactly one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (freed).
These three rules, taken together, eliminate memory leaks, use-after-free bugs, and double-frees — the most common sources of security vulnerabilities in systems software.
Scope and Drop
The most immediate consequence of ownership is deterministic memory management. When a variable goes out of scope, its value is automatically freed. There is no free() to forget, no garbage collector to wait for — the compiler inserts the cleanup code exactly where it is needed.
fn main() {
{
let s = String::from("hello"); // s owns the String — heap memory is allocated
println!("{}", s);
} // s goes out of scope here — drop(s) is called automatically
// the heap memory is freed at this exact point
// println!("{}", s); // ERROR: s is no longer in scope
}
$ rustc ownership_scope_and_drop.rs && ./ownership_scope_and_drop
hello
drop is called automatically by the compiler at the closing brace of the owner’s scope. This is deterministic — you always know exactly when memory is freed.
Stack vs Heap
Understanding the stack/heap distinction is essential to understanding ownership, because the two memory regions have fundamentally different characteristics that drive Rust’s design decisions.
- Stack: Fixed-size data known at compile time. Push/pop is extremely fast. Integers, floats, booleans, chars, fixed-size arrays all live here.
- Heap: Data whose size is unknown at compile time or may change at runtime. Allocated through an allocator, must be explicitly freed.
String,Vec,Boxall store their data on the heap.
use std::mem::size_of;
fn main() {
let x = 5; // stored entirely on the stack
let s = String::from("hello"); // s (a 3-word struct) is on the stack,
// but the actual string bytes are on the heap
println!("x = {}, s = {}", x, s);
println!("size_of::<i32>() = {} bytes", size_of::<i32>());
println!("size_of::<String>() = {} bytes", size_of::<String>());
println!("s.len() = {}, s.capacity() = {}", s.len(), s.capacity());
}
$ rustc ownership_stack_vs_heap.rs && ./ownership_stack_vs_heap
x = 5, s = hello
size_of::<i32>() = 4 bytes
size_of::<String>() = 24 bytes
s.len() = 5, s.capacity() = 5
The String itself is always 24 bytes on a 64-bit target — a pointer, a length and a capacity, three machine words — no matter how long the text is. The five bytes of "hello" live on the heap at the address that pointer holds; len and capacity are both 5 because String::from allocates exactly what it needs.
Move Semantics
When you assign a heap-allocated value to another variable, ownership moves — the original binding is invalidated and the new variable becomes the sole owner. This prevents double-free errors: because there is always exactly one owner, there is always exactly one drop. Rust does not silently copy heap data — the assignment is cheap (just copies the stack-stored pointer/length/capacity) and the original becomes unusable.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2 — s1 is no longer valid
println!("{}", s1); // ERROR: value moved to s2
println!("{}", s2);
}
$ rustc ownership_move_semantics.rs
error[E0382]: borrow of moved value: `s1`
--> ownership_move_semantics.rs:5:20
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1; // ownership moves to s2 — s1 is no longer valid
| -- value moved here
4 |
5 | println!("{}", s1); // ERROR: value moved to s2
| ^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone(); // ownership moves to s2 — s1 is no longer valid
| ++++++++
The error points at three places: where s1 got its String, where it moved (let s2 = s1), and where it was used after the move. Nothing runs — the program never reaches println! because it never compiles. The fix is to stop using s1 once it has moved:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2 — s1 is no longer valid
println!("{}", s2); // OK — s2 is the owner now
}
$ rustc ownership_move_semantics_2.rs && ./ownership_move_semantics_2
hello
The same move happens when you pass a value to a function — the function becomes the new owner:
fn take_ownership(s: String) {
println!("got: {}", s);
} // s is the owner here, so it is dropped when this function returns
fn main() {
let s = String::from("world");
take_ownership(s); // s is moved into the function
// println!("{}", s); // ERROR: s was moved — it no longer exists here
}
$ rustc ownership_move_semantics_3.rs && ./ownership_move_semantics_3
got: world
And when returning from a function, ownership moves back to the caller:
fn give_ownership() -> String {
let s = String::from("hello");
s // move ownership to the caller — the String is not dropped here
}
fn take_and_give_back(s: String) -> String {
s // move the received value back to the caller
}
fn main() {
let s1 = give_ownership(); // s1 becomes owner
let s2 = String::from("world");
let s3 = take_and_give_back(s2); // s2 is moved in, s3 becomes owner of the result
println!("{} {}", s1, s3);
}
$ rustc ownership_move_semantics_4.rs && ./ownership_move_semantics_4
hello world
This is why Rust introduces borrowing — passing ownership around for every function call is tedious. But first, understand the Copy trait.
The Copy Trait
Types that are entirely stack-allocated and trivially copyable implement the Copy trait. For these types, assignment copies the value bitwise instead of moving ownership — both the original and the new binding remain valid. This is why you can use x after let y = x when x is an integer, even though you cannot do the same with a String.
fn main() {
let x = 5;
let y = x; // x is COPIED (not moved) because i32 implements Copy
println!("{} {}", x, y); // both still valid
}
$ rustc ownership_the_copy_trait.rs && ./ownership_the_copy_trait
5 5
Types that implement Copy:
- All integer types:
i8,i16,i32,i64,i128,u8,u16,u32,u64,u128,isize,usize - Floating-point:
f32,f64 bool,char- Tuples and arrays only if all their elements are
Copy - Shared references
&T
Types that do not implement Copy because they manage heap resources:
StringVec<T>Box<T>HashMap,HashSet- Any type containing a non-
Copyfield
Cloning
When you genuinely need an independent copy of a non-Copy type — rather than just a reference — use .clone(). Clone is explicit by design: it signals at the call site that a potentially expensive heap allocation is happening, so readers immediately know this is not a cheap operation.
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // deep copy — allocates new heap memory for s2's data
println!("{} {}", s1, s2); // both valid — s1 and s2 are independent owners
}
$ rustc ownership_cloning.rs && ./ownership_cloning
hello hello
Drop and RAII
Rust implements the RAII (Resource Acquisition Is Initialization) pattern. When an owner goes out of scope, Drop::drop is called on it. This is not just about memory — it applies to any resource. File handles are closed, network sockets are released, mutex locks are unlocked, all automatically when the owning variable leaves scope.
use std::fs::{self, File};
use std::io::Write;
fn main() {
{
let mut f = File::create("temp.txt").unwrap(); // file is opened (resource acquired)
writeln!(f, "written before the drop").unwrap();
} // f is dropped here — file is flushed and closed automatically, no close() needed
let contents = fs::read_to_string("temp.txt").unwrap();
print!("read back: {}", contents);
fs::remove_file("temp.txt").unwrap();
}
$ rustc ownership_drop_and_raii.rs && ./ownership_drop_and_raii
read back: written before the drop
You can implement Drop for your own types to run custom cleanup logic:
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
// This runs automatically when the Resource goes out of scope
println!("Dropping resource: {}", self.name);
}
}
fn main() {
let _r1 = Resource { name: String::from("A") };
{
let _r2 = Resource { name: String::from("B") };
println!("Inside inner block");
} // _r2 dropped here
println!("Back in outer block");
} // _r1 dropped here
$ rustc ownership_drop_and_raii_2.rs && ./ownership_drop_and_raii_2
Inside inner block
Dropping resource: B
Back in outer block
Dropping resource: A
B is dropped the moment its inner block closes, before “Back in outer block” prints; A is dropped last, when main returns. The leading underscore in _r1 only silences the unused-variable warning — the value still lives until the end of its scope.
When several values share one scope, they are dropped in reverse order of declaration — like a stack unwinding.
Forcing Early Drop
The standard library’s drop(value) function lets you drop a value before the end of its scope. This is useful when you need to release a resource — like a mutex lock — before the scope ends so that other code can acquire it.
fn main() {
let s = String::from("hello");
println!("created: {}", s);
drop(s); // explicitly drop s before the end of the block
// println!("{}", s); // ERROR: s has already been dropped
println!("s has been dropped early");
}
$ rustc ownership_forcing_early_drop.rs && ./ownership_forcing_early_drop
created: hello
s has been dropped early
Ownership and Functions: Summary
fn main() {
// Copy type — assignment copies, both bindings remain valid
let n = 42;
let m = n;
println!("{} {}", n, m); // both valid
// Non-Copy type — assignment moves, original is invalidated
let v = vec![1, 2, 3];
let w = v;
// println!("{:?}", v); // ERROR: v was moved
println!("{:?}", w);
// Non-Copy type — clone for an explicit independent copy
let a = String::from("hello");
let b = a.clone();
println!("{} {}", a, b); // both valid — independent heap allocations
// Passing a non-Copy value to a function moves it
let s = String::from("world");
let len = calculate_length(s); // s is moved into calculate_length
// s is no longer accessible here
println!("length: {}", len);
// Use references (borrowing) to avoid moving — see next tutorial
}
fn calculate_length(s: String) -> usize {
s.len()
} // s is dropped here — the String is freed
$ rustc ownership_ownership_and_functions_summary.rs && ./ownership_ownership_and_functions_summary
42 42
[1, 2, 3]
hello hello
length: 5
The next tutorial covers borrowing and references, which let you use values without transferring ownership.
Practice
-
In the
Resourceexample, declare a third valuelet _r3 = Resource { name: String::from("C") };right after_r1, in the outer scope. Predict the full output before running it.Solution
Inside inner block Dropping resource: B Back in outer block Dropping resource: C Dropping resource: AAandCshare a scope, so they drop in reverse declaration order:C(declared last) goes first, thenA. -
Change
let _r1 = Resource { ... }tolet _ = Resource { ... }. What moves in the output, and why?Solution
Dropping resource: A Inside inner block Dropping resource: B Back in outer block_is not a variable name — it is a pattern that binds nothing. With no owner to hold it, theResourceis dropped at the end of that statement, before anything else prints._r1is a real binding that lives to the end ofmain. -
In the
take_ownershipexample, uncomment theprintln!("{}", s);after the call. Which error code does the compiler report, and what two fixes does it suggest?Solution
error[E0382]: borrow of moved value: \s`. The compiler suggests changing the parameter to borrow instead ("consider changing this parameter type in functiontake_ownershipto borrow instead if owning the value isn't necessary") and, failing that,take_ownership(s.clone()). The first fix is the one the next lesson teaches:fn take_ownership(s: &str)`. -
Define
#[derive(Debug, Clone, Copy)] struct Point { x: i32, y: i32 }, assignlet b = a;, and print both. Then add alabel: Stringfield. What happens?Solution
With two
i32fields,println!("{:?} {:?}", a, b)printsPoint { x: 1, y: 2 } Point { x: 1, y: 2 }—ais still usable because it was copied. Addinglabel: Stringfails to compile:error[E0204]: the trait `Copy` cannot be implemented for this type ... 5 | label: String, | ------------- this field does not implement `Copy`A type can only be
Copyif every field isCopy; one heap-owning field is enough to forbid it.