Core Concepts of Rust

Borrow Checker, Rust compiler checks the code memory safety at compile time. There is no runtime overhead.

Ownership

  • Each value has a single owner: In Rust, every piece of data 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. When ownership is transferred (e.g., by assigning a variable to another), the original owner loses access to the value. This is known as a “move.”
  • When the owner goes out of scope, the value is dropped: When the variable that owns a value goes out of scope, Rust automatically deallocates the memory associated with that value. This prevents memory leaks and ensures memory safety.
// This transfers the ownership of s
fn print_string(s: String) {
    println!("{s}");
}

// Borrowing: this does not transfer the ownership of s, just borrows the value
fn print_string_borrowed(s: &String) {
    println!("{s}");
}

fn print_string_slice(s: &str) {
    println!("{s}");
}

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.

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 (no dangling pointers).
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.