Languages
Rust
Ownership System

Rust's Ownership System: Memory Safety Without a Garbage Collector

fn main() {
    let s = String::from("hello");  // s owns the String data on the heap
    takes_ownership(s);             // s's ownership is moved into the function
    // println!("{}", s);           // Error! s is no longer valid here
}
 
fn takes_ownership(s: String) {     // s now owns the String
    println!("{}", s);
}                                   // s is dropped, memory is freed

The Three Core Rules of Ownership

  1. Each value has a single owner
    • When the owner goes out of scope, the value is dropped (drop is called)
  2. Only one owner at a time
    • Assigning a value to another variable moves it (not a shallow copy)
  3. No dangling references
    • The borrow checker ensures references always point to valid data

Why Ownership Matters

  • Prevents memory leaks: Automatic deallocation when owners go out of scope
  • Eliminates data races: The compiler enforces exclusive mutable access
  • No garbage collector needed: Predictable performance with zero-cost abstractions

Borrowing: Sharing Without Moving

Rust allows references to data without taking ownership:

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s);  // Pass a reference
    println!("Length of '{}' is {}", s, len);  // s is still valid
}
 
fn calculate_length(s: &String) -> usize {  // Borrows the String
    s.len()
}  // s goes out of scope but doesn't drop the String (not the owner)

The Borrowing Rules

  1. Either:
    • Any number of immutable references (&T)
    • Or: Exactly one mutable reference (&mut T)
  2. References must always be valid
    • The borrow checker enforces this at compile time

Lifetimes: Ensuring Reference Validity

Lifetimes annotate how long references remain valid:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
 
fn main() {
    let s1 = String::from("long");
    let result;
    {
        let s2 = String::from("short");
        result = longest(s1.as_str(), s2.as_str());
        println!("Longest: {}", result);  // OK
    }
    // println!("Longest: {}", result);  // Error! s2's lifetime ended
}

Lifetime Elision Rules

In many cases, Rust can infer lifetimes automatically:

  1. Each parameter gets its own lifetime if unspecified
  2. If there's exactly one input lifetime, it's assigned to all outputs
  3. For methods, &self or &mut self assigns lifetimes to all outputs

Common Ownership Patterns

1. Returning Ownership

fn create_string() -> String {
    let s = String::from("new string");
    s  // Ownership is returned to the caller
}

2. Structs with References (Requires Lifetimes)

struct Excerpt<'a> {
    part: &'a str,
}
 
fn main() {
    let novel = String::from("Call me Ishmael...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = Excerpt { part: first_sentence };
}

3. Cloning for Deep Copies

let s1 = String::from("hello");
let s2 = s1.clone();  // Explicit deep copy (expensive but sometimes necessary)

Performance Implications

  • Zero-cost abstractions: Ownership checks happen at compile time
  • No runtime overhead: No garbage collector or reference counting
  • Predictable performance: Memory management is explicit

Comparison to Other Languages

FeatureRustC/C++Java/Python
Memory SafetyCompiler-enforcedManualGC
Dangling PointersImpossiblePossibleImpossible
Data RacesPreventedPossiblePossible
Runtime OverheadNoneNoneGC Pauses

Advanced Topics

1. Smart Pointers

  • Box<T>: Heap allocation with single ownership
  • Rc<T>: Reference counting for multiple ownership (immutable)
  • Arc<T>: Thread-safe reference counting
  • RefCell<T>: Interior mutability with runtime checks

2. Lifetime Bounds

struct Context<'a, 'b: 'a> {  // 'b must outlive 'a
    data: &'a &'b str,
}

3. The 'static Lifetime

let s: &'static str = "I live forever";  // Stored in binary

Best Practices

  1. Prefer borrowing over cloning when possible
  2. Use lifetimes explicitly only when elision doesn't work
  3. Leverage the compiler's error messages to fix ownership issues
  4. Consider Rc<T> or Arc<T> when shared ownership is truly needed
  5. Use tools like clippy to catch common ownership mistakes

Further Reading

Last updated on April 10, 2025