Core Concepts of Ownership

Move Semantics

When a value is assigned to a new variable, Rust moves the ownership rather than copying the data. This behavior prevents accidental duplication of expensive resources while ensuring memory safety.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 is moved to s2
    // println!("{}", s1); // This would cause a compile error
    println!("{}", s2);
}

The key insight is that s1 no longer owns the string data after the assignment. This move operation is cheap because it only transfers a pointer and metadata, not the actual data.

Copy vs Move

Not all types are moved when assigned. Types that implement the Copy trait are automatically copied, not moved.

fn main() {
    let x = 5;
    let y = x; // x is copied, not moved
    println!("x = {}, y = {}", x, y); // Both variables are valid
    
    let s1 = String::from("hello");
    let s2 = s1; // s1 is moved to s2
    // println!("{}", s1); // Error: s1 no longer owns the data
}

The following table summarizes which types are copied versus moved:

TypeCopyReason
i32, i64, f32, f64YesFixed-size, stack-allocated
boolYesSingle bit representation
charYesUnicode scalar value
&T and &mut TYesReferences are copied
StringNoHeap-allocated, expensive to copy
Vec<T>NoDynamic array with heap allocation
ArraysYesOnly if elements are Copy

Borrowing and References

When you need to access a value without taking ownership, you can borrow it using references. There are two types of references: immutable (&T) and mutable (&mut T).

fn main() {
    let s = String::from("hello");
    
    // Immutable borrow
    let len = calculate_length(&s);
    println!("The length of '{}' is {}.", s, len);
    
    // Mutable borrow
    let mut s2 = String::from("hello");
    change(&mut s2);
    println!("{}", s2);
}

fn calculate_length(s: &String) -> usize {
    s.len() // s is a reference to String
}

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

Advanced Ownership Patterns

Function Parameters and Ownership

Function parameters follow the same move and borrow rules as variable assignments.

fn main() {
    let s = String::from("hello");
    
    // Pass by value (move)
    takes_ownership(s);
    // println!("{}", s); // Error: s was moved
    
    let x = 5;
    // Pass by copy
    makes_copy(x);
    println!("x = {}", x); // Valid: x was copied
}

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

fn makes_copy(i: i32) {
    println!("{}", i);
} // i is copied, so no drop occurs

Returning Ownership

Functions can also transfer ownership back to the caller.

fn main() {
    let s1 = String::from("hello");
    let s2 = gives_ownership(s1);
    println!("{}", s2);
}

fn gives_ownership(s: String) -> String {
    s // Move s to the caller
}

Multiple References

Rust's borrowing rules prevent data races by allowing either multiple immutable references OR one mutable reference, but not both simultaneously.

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s; // immutable reference
    let r2 = &s; // immutable reference
    println!("{} and {}", r1, r2); // Both valid
    
    // let r3 = &mut s; // Error: cannot borrow mutably while immutable references exist
    
    let r3 = &mut s; // mutable reference
    r3.push_str(" world");
    println!("{}", r3); // Valid
}

Best Practices and Common Pitfalls

Avoiding Unnecessary Moves

When you need to use a value multiple times, consider borrowing instead of moving.

// Inefficient: moving the string each time
fn bad_example(s: String) -> usize {
    s.len() + s.len() + s.len()
}

// Efficient: borrowing the string
fn good_example(s: &String) -> usize {
    s.len() + s.len() + s.len()
}

// Even better: accepting a string slice
fn best_example(s: &str) -> usize {
    s.len() + s.len() + s.len()
}

Working with Collections

When working with collections, understand when you need to move vs borrow.

fn main() {
    let vec = vec![1, 2, 3, 4, 5];
    
    // Move the vector
    process_vector(vec);
    
    // Or borrow it
    let vec2 = vec![1, 2, 3, 4, 5];
    let sum = calculate_sum(&vec2);
    println!("Sum: {}", sum);
}

fn process_vector(v: Vec<i32>) {
    // v is moved here
    println!("Processing vector with {} elements", v.len());
}

fn calculate_sum(v: &Vec<i32>) -> i32 {
    v.iter().sum()
}

Memory Layout and Performance

Rust's ownership system ensures that memory layout is predictable and efficient. The compiler can optimize based on ownership patterns, often eliminating unnecessary allocations and copies.

fn main() {
    // Stack allocation for primitive types
    let x = 42;
    
    // Heap allocation for owned types
    let s = String::from("hello");
    
    // References point to existing memory
    let r = &s;
    
    // No copying occurs - only pointer manipulation
    let r2 = r;
}

Understanding ownership is crucial for writing efficient, safe Rust code. The system prevents memory errors at compile time while allowing fine-grained control over resource management. As you progress with Rust, these concepts will become second nature, enabling you to write high-performance code without sacrificing safety.

Learn more with useful resources