ownership is a core concept that determines how memory is managed at runtime, guaranteeing memory safety without needing a garbage collector. Each value in Rust has a single owner (a variable), and when the owner goes out of scope, the value is dropped (memory is freed). This system prevents memory leaks and dangling pointers by ensuring that there's always a clear responsibility for cleaning up memory

Ownership Rules

  • Each value in Rust has a variable that’s called its owner.
  • There can be only one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Example: Ownership Transfer

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // Ownership moves from s1 to s2

    println!("{}", s1); // ❌ ERROR: s1 is no longer valid
}

reason for S1 is not valid is String is heap-allocated, and Rust moves ownership to avoid double-free errors. But if you want to keep both s1 and s2 are valid then can use clone

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

println!("{}", s1); 
println!("{}", s2);

Ownership can be returned

fn give_ownership() -> String {
    String::from("Rust")
}

fn main() {
    let s = give_ownership(); // s gets ownership
    println!("{}", s);        // Works fine
}

Or pass in a value and return it back

fn take_and_return(s: String) -> String {
    s // return ownership
}

fn main() {
    let name = String::from("Rust");
    let name = take_and_return(name); // ownership moves and returns
    println!("{}", name); // Still valid
}

Copy Types vs Move Types

  • Simple types like integers are Copy types (stack-based):
  • Complex types like String, Vec, etc., are Move types (heap-based).

simple scalar values can be Copy, and nothing that requires allocation or is some form of resource is Copy. Here are some of the types that are Copy:

• All the integer types, such as u32.
• The Boolean type, bool, with values true and false.
• The character type, char.
• All the floating point types, such as f64.
• Tuples, but only if they contain types that are also Copy. For example, (i32, i32) is Copy, but (i32, String) is not.

References and Borrowing
In Rust, borrowing is when you let someone use a value without taking ownership.

Instead of moving ownership (which makes the original variable unusable), you can borrow it using references.

fn print_name(name: &String) {
    println!("Name: {}", name);
}

fn main() {
    let name = String::from("martin");
    print_name(&name);       // borrowed using & (immutable reference)
    println!("{}", name);    // still usable here
}

Types of Borrowing
Immutable Borrow (&T)

  • Allows read-only access
  • Any number of immutable borrows allowed at the same time
fn show(val: &String) {
    println!("Value: {}", val);
}

Mutable Borrow (&mut T)

  • Allows write access
  • Only one mutable borrow is allowed at a time
fn change(val: &mut String) {
    val.push_str(" Rocks!");
}

fn main() {
    let mut msg = String::from("Rust");
    change(&mut msg);           // mutable borrow
    println!("{}", msg);        // Rust Rocks!
}

Borrowing Rules

  • You can have either one mutable reference or any number of immutable references, but not both at the same time
  • References must always be valid (no dangling pointers!)

Why Borrowing Matters

  • Rust avoids data races and null/dangling pointers
  • You don’t need to manually manage memory
  • It prevents bugs at compile time, not at runtime