Understanding Ownership in Rust

Ownership is a way to manage memory.

There are three ways to manage memory:

  1. Garbage collection (Java/Go)
  2. Manual memory management (C/C++)
  3. Ownership model (Rust)

Rust’s ownership system is a core feature designed to ensure memory safety without relying on a garbage collector. It’s a set of rules the compiler checks at compile time to manage how memory is handled.

Ownership is Rust’s most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector.

Key Principles of Ownership

  • Every value has an owner: In Rust, every piece of data in memory has a variable that is designated as its owner.
  • There can only be one owner at a time: A value cannot have multiple owners simultaneously. This prevents data races and ensures a clear responsibility for memory management.
  • When the owner goes out of scope, the value is dropped: When the variable that owns a piece of data goes out of its defined scope, Rust automatically deallocates the memory associated with that data. This prevents memory leaks.

Rust by default moves a value.

pub fn test() {
    let x = String::from("hello");
    let y = x;

    println!("value is {}", x); // Error, "hello"'s owner is moved to y from x.

    let x = 5;
    let y = x; // Copy, ok
    println!("value is {}", x); // OK, integer is a copy
}

Borrowing

Borrowing allows you to temporarily access data without taking ownership. There are two types of borrows: immutable and mutable.

  • Immutable borrows (&T): Allow read-only access. You can have multiple immutable borrows at the same time.
  • Mutable borrows (&mut T): Allow read-write access, but only one mutable borrow is allowed at a time, and no immutable borrows can coexist.

The Rules of References (Borrowing Rules)

  1. At any given time, you can have either one mutable reference or any number of immutable references.
  2. References must always be valid.
fn main() {
    let mut s = String::from("hello");
    
    // Immutable borrow
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2); // Works fine
    
    // Mutable borrow - would fail if r1 and r2 were still in scope
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{}", r3);
}

Lifetimes

Lifetimes refer to the span of the program during which a reference to a piece of data is valid. They prevent dangling references by ensuring that references do not outlive the data they point to.

  • Lifetime annotations: Explicitly specify lifetimes using syntax like &'a T or &'a mut T.
  • Lifetime elision: In many cases, Rust can infer lifetimes automatically.
  • Static lifetime: 'static indicates data that lives for the entire program duration.
// Function with explicit lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    // println!("The longest string is {}", result); // This would fail because string2 is out of scope
}

These concepts form the foundation of Rust’s memory safety guarantees without a garbage collector.

  1. Each parameter that is a reference gets its own lifetime parameter
  2. If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters;
  3. If there are multiple input lifetime parameters, but one of them is &self or &mut self the lifetime of self is assigned to all output lifetime parameters.

Resources