Ownership and Borrowing

1. Welcome and Icebreaker:

Welcome back! This is a big topic, so get ready!

Imagine you have a valuable object that you want to share with multiple friends, but you want to make sure only one friend can use it at a time, and that it’s always returned to you safely. How would you manage this sharing process? (Think about this in real-world terms, not code yet!)

2. Outline the Session:

Here’s our plan:

  1. Concept Explanation: We’ll introduce the core rules of ownership, borrowing, and lifetimes. We’ll use analogies and diagrams to illustrate these concepts.
  2. Live Coding Demonstration: We’ll write code examples that demonstrate ownership, moves, borrowing (mutable and immutable), and how the borrow checker enforces the rules.
  3. Tasks: I will challenge you in task section.

Learning Outcomes: By the end of this session, you will be able to:

  • Understand the three core rules of Rust’s ownership system.
  • Explain the concepts of moving and borrowing.
  • Differentiate between mutable and immutable borrows.
  • Understand the role of the borrow checker.
  • Write code that adheres to the ownership and borrowing rules.
  • Recognize and fix common ownership-related compiler errors.

Expectations: This is a conceptually challenging topic. Don’t be discouraged if it takes some time to grasp! Ask questions, experiment with the code, and try to break things – that’s how you’ll learn.

3. Concept Explanation:

Rust’s ownership system is based on three fundamental rules:

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped. (Dropped means the memory is freed.)

These rules are enforced by the borrow checker, a part of the Rust compiler.

Analogy:

Think of ownership like owning a physical book:

  • Rule 1: You (a variable) own the book (a value).
  • Rule 2: Only one person can own the book at any given time.
  • Rule 3: When you’re done with the book (you go out of scope), you put it away (the book is “dropped”).

Moving:

When you assign a value to another variable, the ownership is moved. The original variable can no longer be used.

let s1 = String::from("hello");
let s2 = s1; // Ownership of the "hello" string is moved from s1 to s2
// println!("{}", s1); // This would cause a compile-time error! s1 is no longer valid

Borrowing:

Instead of moving ownership, you can borrow a value. Borrowing allows you to access a value without taking ownership. There are two types of borrows:

  • Immutable Borrows (&): Allow you to read the value, but not modify it. You can have multiple immutable borrows at the same time.
  • Mutable Borrows (&mut): Allow you to both read and modify the value. You can only have one mutable borrow at a time, and no other borrows (mutable or immutable) can exist simultaneously.

Borrowing Rules (Summary):

  • You can have any number of immutable borrows (&).
  • You can have only one mutable borrow (&mut).
  • You cannot have a mutable borrow while any immutable borrows exist.

Lifetimes (Brief Introduction):

Lifetimes are a way of ensuring that borrows are always valid. A lifetime is a construct that the borrow checker uses to track how long a reference is valid. You don’t always need to write lifetimes explicitly, as the compiler can often infer them. The most important rule about lifetimes is:

  • A reference cannot outlive the data it points to.

4. Live Coding Demonstration:

fn main() {
    // --- Ownership and Moving ---
    let s1 = String::from("hello"); // s1 owns the string "hello"
    let s2 = s1; // Ownership is moved to s2

    // println!("{}", s1); // ERROR: Use of moved value: `s1`
    println!("{}", s2);    // This is valid because s2 now owns the string

    // --- Immutable Borrows ---
    let s3 = String::from("world");
    let r1 = &s3; // Immutable borrow of s3
    let r2 = &s3; // Another immutable borrow of s3 (allowed)
    println!("{} {} {}", s3, r1, r2); // All are valid

    // --- Mutable Borrows ---
    let mut s4 = String::from("change");
    let r3 = &mut s4; // Mutable borrow of s4
    *r3 = String::from("modified"); // Modify the string through the mutable borrow
    println!("{}", r3);          // Valid: we're using the mutable borrow
    // let r4 = &mut s4;        // ERROR: Cannot borrow `s4` as mutable more than once at a time
    // println!("{}", s4);       // ERROR: Cannot borrow `s4` as immutable because it is also borrowed as mutable

    // --- Borrowing and Scope ---
    {
        let s5 = String::from("inner scope");
        let r5 = &s5;
        println!("{}", r5);     // Valid: r5 is within the scope where s5 is valid
    } // s5 is dropped here, and r5 is no longer valid
    // println!("{}", r5);  // ERROR: Borrowed value does not live long enough

    // --- Function and Ownership ---
    let my_string = gives_ownership();
    println!("{}",my_string);

    let my_another_string = String::from("Another");
    let new_string = takes_and_gives_back(my_another_string);
    //println!("{}",my_another_string);  //ERROR: my_another_string moved
    println!("{}",new_string);

    let mut my_mut_string = String::from("Mut String");
    println!("Before change: {}",my_mut_string);
    change_string(&mut my_mut_string);
    println!("After change: {}",my_mut_string);

}

fn gives_ownership() -> String {             // gives_ownership will move its
    let some_string = String::from("hello"); // return value into the function
    some_string                              // that calls it
}

fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
    a_string  // a_string is returned and moves out to the calling function
}

fn change_string(value: &mut String){
    value.push_str(" Changed!!")
}

Key Takeaways:

  • Moving: let s2 = s1; moves ownership. s1 becomes invalid.
  • Immutable Borrows: &s3 creates an immutable borrow. Multiple immutable borrows are allowed.
  • Mutable Borrows: &mut s4 creates a mutable borrow. Only one is allowed at a time.
  • Borrow Checker Errors: The commented-out lines demonstrate common errors that the borrow checker will catch at compile time.
  • Scope: Borrows must be valid within the scope of the owner.
  • Function: Function can take or gives ownership.

5. Tasks

  1. Fix the Errors: Uncomment the error lines in the code above, one at a time, and read the compiler error messages. Try to understand why the error occurs based on the ownership and borrowing rules. Then, modify the code to fix the error without deleting the uncommented line (you might need to add new lines or change existing ones).
  2. Multiple Immutable Borrows: Write a function that takes a String as an argument using an immutable borrow (&String). Within the function, print the string. Call the function multiple times with the same String variable to demonstrate that multiple immutable borrows are allowed.
  3. Mutable Borrow and Modification: Write a function that takes a String as an argument using a mutable borrow (&mut String). Within the function, modify the string (e.g., append something to it). Call the function and then print the original string to show that it has been modified.
  4. Ownership Transfer: Write a function that takes ownership of a String (i.e., the argument type is String, not &String or &mut String). Inside the function, print the string. After calling this function, try to use the original String variable. Observe the compiler error.
  5. Borrowing and Returning: Write a function that takes a String as an argument using an immutable borrow and returns an immutable borrow of the same String. This demonstrates how to return references from functions.
  6. Complex Scenario:
    • Create a String variable named original.
    • Create a function process_string that takes a mutable borrow of a String.
    • Inside process_string, create another (inner) scope.
    • Inside the inner scope, create an immutable borrow of the String.
    • Try to modify the String through the mutable borrow within the inner scope. Observe the compiler error.
    • Explain why this error occurs, based on the borrowing rules.
    • Modify the code to make it work correctly, while still demonstrating both mutable and immutable borrows within different scopes.