Rust - Ownership Basics
Overview
Estimated time: 50–60 minutes
Master Rust's ownership system - the key feature that enables memory safety without garbage collection. Learn ownership rules, move semantics, stack vs heap allocation, and how Rust prevents common memory bugs at compile time.
Learning Objectives
- Understand Rust's three ownership rules.
- Learn the difference between stack and heap memory.
- Master move semantics and when values are moved vs copied.
- Understand ownership transfer in function calls and returns.
- Recognize how ownership prevents memory safety issues.
Prerequisites
What is Ownership?
Ownership is Rust's most unique feature. It enables Rust to make memory safety guarantees without needing a garbage collector. Understanding ownership is crucial to writing effective Rust code.
The Three Rules of Ownership
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Stack vs Heap Memory
Stack Memory
- Fast: Last in, first out (LIFO)
- Fixed size: Known at compile time
- Automatic cleanup: Values popped when out of scope
- Examples: integers, floats, booleans, fixed arrays
fn main() {
let x = 42; // Stored on stack
let y = 3.14; // Stored on stack
let arr = [1, 2, 3]; // Fixed array on stack
println!("x: {}, y: {}, arr: {:?}", x, y, arr);
}
Expected Output:
x: 42, y: 3.14, arr: [1, 2, 3]
Heap Memory
- Flexible: Can grow and shrink at runtime
- Dynamic size: Size unknown at compile time
- Manual management: Must be explicitly freed
- Examples: String, Vec, HashMap
fn main() {
let s = String::from("hello"); // Stored on heap
let mut v = Vec::new(); // Stored on heap
v.push(1);
v.push(2);
println!("String: {}, Vector: {:?}", s, v);
}
Expected Output:
String: hello, Vector: [1, 2]
Ownership with Stack Data
Copy Semantics
Simple types implement the Copy
trait and are copied rather than moved:
fn main() {
let x = 5;
let y = x; // x is copied to y
// Both x and y are valid
println!("x: {}, y: {}", x, y);
// Function calls also copy
print_number(x);
println!("x is still valid: {}", x);
}
fn print_number(n: i32) {
println!("Number: {}", n);
}
Expected Output:
x: 5, y: 5
Number: 5
x is still valid: 5
Types that Implement Copy
- All integer types (i32, u32, etc.)
- Boolean type (bool)
- All floating point types (f32, f64)
- Character type (char)
- Tuples, if they only contain Copy types
fn main() {
let tuple = (5, true, 'a');
let tuple_copy = tuple; // Copied because all elements are Copy
println!("Original: {:?}", tuple);
println!("Copy: {:?}", tuple_copy);
}
Expected Output:
Original: (5, true, 'a')
Copy: (5, true, 'a')
Ownership with Heap Data
Move Semantics
Heap-allocated data is moved, not copied, to prevent double-free errors:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
println!("s2: {}", s2);
// println!("s1: {}", s1); // Error: s1 no longer valid
}
Expected Output:
s2: hello
What Happens During a Move
fn main() {
// Memory layout visualization
let s1 = String::from("hello");
// s1: [ptr | len | capacity] -> heap: "hello"
let s2 = s1; // Move occurs
// s1: [invalid]
// s2: [ptr | len | capacity] -> heap: "hello"
// Only s2 owns the heap data now
println!("s2: {}", s2);
}
Preventing Use After Move
Rust prevents use-after-move at compile time:
fn main() {
let s = String::from("hello");
let t = s; // s is moved to t
// This would cause a compile error:
// println!("s: {}", s); // Error: value borrowed here after move
println!("t: {}", t); // This works fine
}
Ownership and Functions
Passing Values to Functions
Passing a value to a function moves or copies it:
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function
// s is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it's okay to still
// use x afterward
println!("x is still valid: {}", x);
}
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
Expected Output:
hello
5
x is still valid: 5
Returning Values
Returning values also transfers ownership:
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
println!("s1: {}, s3: {}", s1, s3);
// s2 is no longer valid, but s1 and s3 are
}
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}
Expected Output:
s1: yours, s3: hello
The Problem with Taking Ownership
Ownership Transfer Issues
Taking ownership in functions can be inconvenient:
fn main() {
let s1 = String::from("hello");
let len = calculate_length_bad(s1); // s1 is moved
// println!("{}", s1); // Error: s1 no longer valid
println!("Length: {}", len);
}
fn calculate_length_bad(s: String) -> usize {
s.len()
} // s goes out of scope and is dropped
Solution: Return Ownership
We can return the ownership along with results:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length_with_return(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length_with_return(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
Expected Output:
The length of 'hello' is 5.
Note: This is tedious! Rust provides references to solve this problem, which we'll cover in the next tutorial.
Scope and Dropping
Automatic Cleanup
Rust automatically calls drop
when variables go out of scope:
fn main() {
{
let s = String::from("hello"); // s comes into scope
// do stuff with s
println!("Inside block: {}", s);
} // s goes out of scope and is dropped
// println!("{}", s); // Error: s is not in scope
println!("s has been dropped");
}
Expected Output:
Inside block: hello
s has been dropped
Multiple Variables and Drop Order
Variables are dropped in reverse order of creation:
fn main() {
let _a = String::from("first");
let _b = String::from("second");
let _c = String::from("third");
println!("All strings created");
// When main ends, variables are dropped in reverse order: _c, _b, _a
}
Expected Output:
All strings created
Clone for Deep Copying
Explicit Cloning
Use clone()
to create a deep copy of heap data:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy - both are valid
println!("s1: {}, s2: {}", s1, s2);
// Both s1 and s2 can be used
use_string(s1.clone());
use_string(s2.clone());
// Original strings still valid
println!("Still valid - s1: {}, s2: {}", s1, s2);
}
fn use_string(s: String) {
println!("Using: {}", s);
}
Expected Output:
s1: hello, s2: hello
Using: hello
Using: hello
Still valid - s1: hello, s2: hello
When to Use Clone
- Rarely in performance-critical code - cloning is expensive
- When you need independent copies - separate ownership
- For learning purposes - to avoid borrowing complexity initially
Ownership Patterns
Builder Pattern with Ownership
struct Config {
name: String,
value: i32,
}
impl Config {
fn new(name: String) -> Self {
Config {
name,
value: 0,
}
}
fn with_value(mut self, value: i32) -> Self {
self.value = value;
self
}
fn build(self) -> Self {
self
}
}
fn main() {
let config = Config::new(String::from("my_config"))
.with_value(42)
.build();
println!("Config: name={}, value={}", config.name, config.value);
}
Expected Output:
Config: name=my_config, value=42
Option and Ownership
fn main() {
let mut maybe_string = Some(String::from("hello"));
// Take ownership from Option
if let Some(s) = maybe_string.take() {
println!("Got string: {}", s);
// s is owned here and will be dropped
}
// maybe_string is now None
println!("maybe_string is now: {:?}", maybe_string);
}
Expected Output:
Got string: hello
maybe_string is now: None
Memory Safety Benefits
Preventing Double Free
Rust prevents double-free errors at compile time:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 moved to s2
// If this were allowed, both s1 and s2 would try to free the same memory
// drop(s1); // Error: use of moved value
drop(s2); // OK: s2 owns the value
}
Preventing Use After Free
fn main() {
let s = String::from("hello");
drop(s); // Explicitly free s
// println!("{}", s); // Error: use after free prevented
}
Preventing Memory Leaks
Automatic cleanup prevents most memory leaks:
fn create_and_forget() {
let _s = String::from("This will be automatically cleaned up");
// No need to manually free memory
} // _s is automatically dropped here
fn main() {
create_and_forget();
println!("Memory was automatically cleaned up");
}
Expected Output:
Memory was automatically cleaned up
Common Ownership Mistakes
Using Moved Values
fn main() {
let s = String::from("hello");
let t = s; // s is moved
// println!("{}", s); // Error: value used after move
println!("{}", t); // OK
}
Moving in Loops
fn main() {
let strings = vec![
String::from("hello"),
String::from("world"),
];
for s in strings { // strings is moved into the loop
println!("{}", s);
}
// println!("{:?}", strings); // Error: strings was moved
}
Expected Output:
hello
world
Partial Moves
struct Point {
x: String,
y: String,
}
fn main() {
let p = Point {
x: String::from("1"),
y: String::from("2"),
};
let x = p.x; // Partial move of p.x
// println!("{}", p.x); // Error: p.x was moved
println!("{}", p.y); // OK: p.y wasn't moved
// println!("{:?}", p); // Error: can't use p because part was moved
}
Expected Output:
2
Best Practices
Design for Ownership
- Think about who should own each piece of data
- Prefer taking ownership when functions need to store data
- Use references when functions only need to read data
- Consider the lifetime of data when designing APIs
Minimize Cloning
- Clone only when necessary (performance cost)
- Use references instead of cloning for temporary access
- Consider
Rc
for shared ownership scenarios
Understand Move Semantics
- Know which types are Copy vs Move
- Be explicit about ownership transfer
- Use type annotations to clarify intent
Next Steps
Ownership is fundamental to Rust, but it can make some tasks inconvenient. The next topics will show you how to work with data without taking ownership:
- References & Borrowing: Access data without taking ownership
- Slices: Work with portions of data
- Lifetimes: Ensure references are valid
Common Pitfalls
- Fighting the ownership system: Work with it, not against it
- Overusing clone(): Often there's a better way with references
- Not understanding stack vs heap: Critical for performance and ownership
- Confusing Copy and Move: Know which types implement Copy
Checks for Understanding
- What are the three rules of ownership in Rust?
- What happens when you assign a String to another variable?
- Why can you use an i32 after passing it to a function, but not a String?
- When is data stored on the stack vs the heap?
- How do you create a deep copy of heap-allocated data?
Answers
- Each value has an owner; only one owner at a time; when owner goes out of scope, value is dropped
- The String is moved to the new variable, making the original invalid
- i32 implements Copy (copied), String doesn't (moved)
- Stack: fixed size known at compile time; Heap: dynamic size, allocated at runtime
- Use the
clone()
method