Loading...
Loading...

Rust Ownership and Borrowing

Ownership is Rust's revolutionary memory management system that enforces strict rules at compile time to guarantee memory safety without garbage collection. Combined with borrowing, these concepts prevent common bugs like dangling pointers, data races, and memory leaks while maintaining performance.

1. The Ownership System

Core Rules:

  1. Each value has a single owner variable
  2. When the owner goes out of scope, the value is dropped
  3. Assignment transfers ownership (moves) by default
fn main() {
    // String owns heap-allocated memory
    let s1 = String::from("hello");
    
    // Ownership moves to s2 - s1 is no longer valid
    let s2 = s1;
    
    // ERROR: value borrowed here after move
    // println!("{}", s1);
    
    // Ownership in functions
    takes_ownership(s2); // s2 moves into function
    // println!("{}", s2); // Would error - s2 invalid
    
    let x = 5;
    makes_copy(x); // i32 is Copy - still valid
}

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

fn makes_copy(n: i32) {
    println!("{}", n);
}

Key Concepts:

  • Heap-allocated types (String, Vec) implement move semantics
  • Stack-allocated types (i32, bool) implement Copy trait
  • Scope boundaries determine when values are dropped

2. References and Borrowing

Borrowing Rules:

  1. You can have either:
    • One mutable reference, OR
    • Any number of immutable references
  2. References must always be valid
fn main() {
    let s = String::from("hello");
    
    // Immutable borrow
    let len = calculate_length(&s);
    println!("Length of '{}': {}", s, len);
    
    // Mutable borrow
    let mut s = String::from("hello");
    change_string(&mut s);
    println!("Modified: {}", s);
}

fn calculate_length(s: &String) -> usize {
    s.len()
    // s goes out of scope but doesn't drop the String
}

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

Why This Matters:

  • Prevents data races at compile time
  • Enables safe concurrent programming
  • No runtime overhead like garbage collection

3. Slices - View Into Data

What They Are: References to contiguous sequences in collections without ownership.

Key Benefit: Safe access to portions of arrays/strings with bounds checking.

fn main() {
    let s = String::from("hello world");
    
    // String slice
    let hello = &s[0..5];
    let world = &s[6..11];
    
    println!("{} {}", hello, world);
    
    // Array slice
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3]; // [2, 3]
    
    // Best practice: accept &str instead of &String
    print_slice(&s[..]); // Whole string as slice
}

fn print_slice(s: &str) {
    println!("Slice: {}", s);
}

Slice Rules:

  • Don't own the data they reference
  • Bounds checked at compile time where possible
  • More flexible than full collection references

4. Ownership Patterns

Returning Ownership: Functions can transfer ownership back to caller.

Struct Ownership: Fields own their data unless using references with lifetimes.

// 1. Returning ownership
fn create_string() -> String {
    let s = String::from("new string");
    s // Ownership returned
}

// 2. Struct ownership
struct Library {
    books: Vec<String>, // Owns book titles
}

impl Library {
    fn add_book(&mut self, title: String) {
        self.books.push(title); // Ownership moved to struct
    }
}

// 3. Avoiding moves with references
fn process_data(data: &[i32]) {
    // Work with borrowed data
}

Common Strategies:

  • Use borrowing where possible to avoid moves
  • Clone data only when necessary
  • Design APIs to minimize ownership transfers

5. Lifetime Annotations

What They Do: Specify how long references must remain valid.

When Needed: When functions return references or structs contain references.

// Function with explicit lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Struct containing reference
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael...");
    let first_sentence = novel.split('.')
        .next()
        .expect("Could not find sentence");
    
    let excerpt = Excerpt {
        part: first_sentence,
    };
    
    println!("Excerpt: {}", excerpt.part);
}

Lifetime Elision Rules:

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

6. Shared Ownership with Smart Pointers

When Needed: For scenarios requiring multiple owners or heap allocation.

use std::rc::Rc;
use std::cell::RefCell;

// 1. Reference Counting (single-threaded)
let rc = Rc::new(String::from("shared"));
let rc_clone = Rc::clone(&rc);

// 2. RefCell for interior mutability
let data = RefCell::new(42);
{
    let mut borrow = data.borrow_mut();
    *borrow += 1;
}

// 3. Arc for thread-safe reference counting
use std::sync::Arc;
let arc = Arc::new(5);
let arc_clone = Arc::clone(&arc);

std::thread::spawn(move || {
    println!("{}", arc_clone);
});

Pointer Types:

  • Box<T>: Simple heap allocation
  • Rc<T>: Single-threaded reference counting
  • Arc<T>: Thread-safe atomic reference counting
  • RefCell<T>: Interior mutability with runtime checks
0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home