Skip to content
All essays
CraftMarch 12, 202412 min

Understanding Rust Ownership System

Master Rust's ownership system for memory safety without garbage collection

Ü
Ümit Uz
Mobile & Full Stack Developer

Rust's ownership system is its most unique and compelling feature. It enables memory safety guarantees without needing a garbage collector, making it possible to write systems-level code that's both safe and performant. Let's dive deep into how ownership works and why it matters.

What is Ownership?

Ownership is a set of rules that govern how Rust manages memory. The compiler enforces these rules at compile-time, preventing entire classes of bugs that plague other systems programming languages.

Core Ownership Rules

Rust has three main ownership rules:

  1. 1Each value has an owner
  2. 2There can only be one owner at a time
  3. 3When the owner goes out of scope, the value is dropped
rust
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // Error! s1 is no longer valid

This move semantics ensures no double-free errors can occur.

Stack vs Heap

Understanding ownership requires knowing the difference between stack and heap memory.

Stack Memory

The stack stores values with a known, fixed size. It's fast and memory-efficient.

rust
let x = 5;
let y = x; // y is a copy of x
// Both x and y are valid

Primitive types implement the Copy trait and remain valid after assignment.

Heap Memory

The heap stores data whose size can change at runtime. It's slower but more flexible.

rust
let s1 = String::from("hello");
let s2 = s1; // s2 now owns the heap data
// s1 is invalidated to prevent double-free

String consists of a pointer, length, and capacity on the stack, pointing to actual data on the heap.

The Borrow Checker

Rust's borrow checker enforces ownership rules at compile-time. It's your friend, not your enemy.

References and Borrowing

Borrowing allows you to reference data without taking ownership.

rust
fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope but has no ownership

let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("Length of '{}' is {}", s1, len);

The & operator creates a reference, allowing you to use data without owning it.

Mutable References

You can have one mutable reference OR multiple immutable references.

rust
let mut s = String::from("hello");

let r1 = &s; // OK
let r2 = &s; // OK
// let r3 = &mut s; // Error! Can't have mutable with immutable

println!("{} and {}", r1, r2);

let r3 = &mut s; // OK, r1 and r2 are no longer used
println!("{}", r3);

This prevents data races at compile-time.

Dangling References

Rust's borrow checker prevents dangling references entirely.

rust
fn dangle() -> &String { // Error!
    let s = String::from("hello");
    &s // Returns reference to value that will be dropped
}

The compiler catches this: "returns a reference to data owned by the current function."

The Fix

Return the owned value instead:

rust
fn no_dangle() -> String {
    let s = String::from("hello");
    s // Ownership is moved to the caller
}

Slice Type

Slices are references to contiguous sections of collections.

rust
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"

String slices are so common that &str is the default string type.

Slice Parameters

Functions often use string slices for flexibility:

rust
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

This works with both String and &str arguments.

Structs and Ownership

Structures can own data, making them powerful for representing complex data.

rust
struct Person {
    name: String,
    age: u32,
}

let p = Person {
    name: String::from("Alice"),
    age: 30,
};

When Person is dropped, its name field is also dropped automatically.

Tuple Structs

Tuple structs are useful for creating new types:

rust
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);

Enums and Ownership

Enums can own data too, making them expressive:

rust
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

let m = Message::Write(String::from("hello"));

Each variant can own different types of data.

Ownership in Functions

Functions can take ownership, borrow immutably, or borrow mutably.

Taking Ownership

rust
fn takes_ownership(s: String) {
    println!("{}", s);
} // s is dropped here

let s = String::from("hello");
takes_ownership(s);
// s is no longer valid

Borrowing

rust
fn borrows(s: &String) {
    println!("{}", s);
}

let s = String::from("hello");
borrows(&s);
// s is still valid

Mutable Borrowing

rust
fn borrows_mutably(s: &mut String) {
    s.push_str(", world");
}

let mut s = String::from("hello");
borrows_mutably(&mut s);

The Clone Trait

When you need to create a deep copy, use clone():

rust
let s1 = String::from("hello");
let s2 = s1.clone();
// Both s1 and s2 are valid

Clone has a runtime cost, so use it deliberately.

Copy Trait

Types implementing Copy can be copied instead of moved:

rust
let x = 5;
let y = x; // Copy, not move
println!("x = {}, y = {}", x, y);

All primitive types implement Copy.

Ownership and Collections

Collections like Vec and HashMap follow ownership rules.

rust
let v = vec![1, 2, 3];
let v2 = v; // v is moved
// println!("{:?}", v); // Error!

Iterating over collections often involves borrowing:

rust
let v = vec![1, 2, 3];
let first = &v[0]; // Immutable borrow
// v.push(4); // Error! Can't borrow mutably while borrowed

Smart Pointers

Smart pointers are data structures that own data and provide additional functionality.

Box<T>

Box<T> allocates data on the heap:

rust
let b = Box::new(5);
println!("b = {}", b);

Useful for recursive types:

rust
enum List {
    Cons(i32, Box<List>),
    Nil,
}

Rc<T>

Rc<T> enables multiple ownership via reference counting:

rust
use std::rc::Rc;

let a = Rc::new(5);
let b = Rc::clone(&a);
let c = Rc::clone(&a);

Only for single-threaded scenarios.

Arc<T>

Arc<T> is like Rc<T> but thread-safe:

rust
use std::sync::Arc;

let a = Arc::new(5);
let b = Arc::clone(&a);

Common Ownership Patterns

The Builder Pattern

Build complex values incrementally:

rust
struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn new() -> Self {
        Circle { x: 0.0, y: 0.0, radius: 1.0 }
    }

    fn x(&mut self, x: f64) -> &mut Self {
        self.x = x;
        self
    }
}

RAII Pattern

Resources are tied to object lifetimes:

rust
struct DatabaseConnection {
    connection: MySqlConnection,
}

impl Drop for DatabaseConnection {
    fn drop(&mut self) {
        self.connection.close();
    }
}

Ownership and Error Handling

Ownership integrates with Rust's error handling:

rust
fn read_file(path: &str) -> Result<String, io::Error> {
    let contents = fs::read_to_string(path)?;
    Ok(contents)
}

The ? operator propagates errors cleanly.

Performance Implications

Ownership enables zero-cost abstractions.

No GC Overhead

No garbage collector means predictable performance:

rust
// Memory is freed immediately when variables go out of scope
{
    let s = String::from("hello");
    // Use s
} // s is dropped here, memory freed

Stack Allocation

Much of Rust's memory usage is stack-allocated:

rust
// No heap allocation
let x = 42;
let y = x;
// Both are valid

Debugging Ownership Issues

The compiler provides excellent error messages.

Common Errors

"borrow of moved value" means you used a value after moving it.

"cannot borrow as mutable" means you have an immutable borrow.

"does not live long enough" means a reference would become dangling.

Fighting the Borrower

Sometimes you need to restructure code:

rust
// Instead of:
let mut v = Vec::new();
v.push(&x);
v.push(&y);

// Consider:
let v = vec![x, y];

Best Practices

  1. 1Prefer borrowing over taking ownership
  2. 2Use shared ownership (Rc/Arc) sparingly
  3. 3Understand the lifetime of your references
  4. 4Use Cow for clone-on-write semantics
  5. 5Design APIs with ownership in mind

Conclusion

Rust's ownership system is its greatest strength. It guarantees memory safety without sacrificing performance, enabling you to write reliable systems code. The rules might seem restrictive at first, but they guide you toward writing better code.

Master ownership, and you'll unlock Rust's full potential for building safe, efficient software. The compiler is your ally, catching entire classes of bugs before they can cause problems in production.

Practice with ownership until it becomes second nature. It's the foundation of everything that makes Rust special.

Enjoyed this essay? Share it.

Related essays

Next essay
Custom React Hooks Guide