Beginner Level

Ownership and Borrowing

Chapter 5: Ownership and Borrowing šŸ”

Ownership is Rust's most unique feature and what makes it memory-safe without a garbage collector. Understanding ownership is crucial to mastering Rust!

What is Ownership?

Ownership is a set of rules that governs how Rust manages memory:

  1. Each value in Rust has an owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value is dropped

The Stack and the Heap

Stack

  • Fast access
  • Fixed size data
  • LIFO (Last In, First Out)
  • Examples: integers, booleans, chars

Heap

  • Slower access
  • Variable size data
  • Allocated at runtime
  • Examples: String, Vec, HashMap

Variable Scope

fn main() {
    {                      // s is not valid here, it's not yet declared
        let s = "hello";   // s is valid from this point forward
        
        // do stuff with s
    }                      // this scope is now over, and s is no longer valid
}

String Type

String Literals vs String Objects

fn main() {
    let s1 = "hello";              // String literal (&str) - immutable
    let s2 = String::from("hello"); // String object - mutable, heap-allocated
    
    let mut s3 = String::from("hello");
    s3.push_str(", world!"); // This works!
    
    println!("{}", s3);
}

Move Semantics

Simple Types (Copy)

fn main() {
    let x = 5;
    let y = x;  // x is copied to y, both are valid
    
    println!("x = {}, y = {}", x, y); // This works!
}

Complex Types (Move)

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

Clone

If you want to deeply copy heap data:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // Deep copy
    
    println!("s1 = {}, s2 = {}", s1, s2); // Both are valid!
}

Ownership and Functions

Passing Values to Functions

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward
    
    println!("x is still valid: {}", x);
    // println!("{}", s); // This would error!
}

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

Return Values and Scope

fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}

References and Borrowing

Instead of taking ownership, you can borrow values:

Immutable References

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);  // Pass a reference

    println!("The length of '{}' is {}.", s1, len); // s1 is still valid!
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.

Mutable References

fn main() {
    let mut s = String::from("hello");

    change(&mut s);  // Pass a mutable reference

    println!("{}", s);
}

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

Rules of References

  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");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);
}

Dangling References

Rust prevents dangling references at compile time:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String
    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger! This won't compile!

Fix by returning the String directly:

fn no_dangle() -> String {
    let s = String::from("hello");
    s // Return ownership
}

Slice Type

Slices let you reference a contiguous sequence of elements:

String Slices

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];  // or &s[..5]
    let world = &s[6..11]; // or &s[6..]
    let whole = &s[..];    // entire string

    println!("{} {} {}", hello, world, whole);
}

Array Slices

fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3];
    
    assert_eq!(slice, &[2, 3]);
}

Practical Examples

Example 1: First Word Function

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    
    println!("First word: {}", word);
    // s.clear(); // This would error because word is still borrowing s
}

Example 2: Largest Element

fn largest(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest(&number_list);
    println!("The largest number is {}", result);
}

Common Ownership Patterns

Pattern 1: Take and Return

fn process_string(s: String) -> String {
    // Process the string
    format!("Processed: {}", s)
}

Pattern 2: Borrow and Process

fn process_string_ref(s: &str) -> String {
    // Process without taking ownership
    format!("Processed: {}", s)
}

Pattern 3: Mutable Borrow

fn modify_string(s: &mut String) {
    s.push_str(" - modified");
}

Practice Exercises

Exercise 1: String Manipulation

Write a function that takes a string and returns it with all vowels removed:

fn remove_vowels(s: &str) -> String {
    // Your implementation here
}

Exercise 2: Vector Operations

Write a function that finds the second largest number in a vector:

fn second_largest(numbers: &[i32]) -> Option<i32> {
    // Your implementation here
}

Key Takeaways

  • āœ… Ownership prevents memory leaks and dangling pointers
  • āœ… Values are moved by default, copied if they implement Copy
  • āœ… Use references (&) to borrow without taking ownership
  • āœ… Mutable references (&mut) allow modification
  • āœ… Can't have mutable and immutable references simultaneously
  • āœ… Slices reference parts of collections safely

Ready for Chapter 6? → Structs

šŸ¦€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS • Open Source