Skip to content

Latest commit

 

History

History
404 lines (336 loc) · 18.7 KB

File metadata and controls

404 lines (336 loc) · 18.7 KB

Understanding Rust Borrowing: A Beginner's Guide 🦀


Step-by-step code walk through, using real-world analogies to make Rust's borrowing easy.

fn calculate_length(s: &String) -> usize {
    s.len()
}

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

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

🏠 Real-World Analogy: Lending a Book

Think of this like lending a book to a friend:

  • You own the book (like s1 owns the String)
  • Your friend borrows it (like the function borrows &s1)
  • You still own it while they're reading it
  • They return it when done (function ends)
  • You can lend it again to someone else

📋 Step-by-Step Breakdown

Step 1: Function Definition

fn calculate_length(s: &String) -> usize {
    s.len()
}

What's happening:

  • s: &String means "s is a reference to a String"
  • The & symbol means "borrow, don't take ownership"
  • Function can read the String but cannot modify or steal it

Step 2: Creating the Owner

let s1 = String::from("hello");

What's happening:

  • s1 owns the String "hello"
  • It's responsible for cleaning up the memory when done
  • Think: "s1 bought the book and owns it"

Step 3: Borrowing (The Magic!)

let len = calculate_length(&s1);

What's happening:

  • &s1 creates a temporary reference to s1's value
  • The function borrows this reference
  • s1 still owns the original String
  • Think: "s1 lends the book to the function to read"

Step 4: Using After Borrowing

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

What's happening:

  • s1 is still valid and can be used!
  • The borrow ended when the function returned
  • Think: "The book was returned, so s1 can use it again"

My commented version

// s: &String means "s is a reference to a String."
// & means borrows the value, not the ownership.
fn calculate_length(s: &String) -> usize {
    s.len()
}

fn main() {
    // Create the owner. s1 owns the String.
    let s1 = String::from("hello");

    // &s1 creates a reference to s1's value.
    let len = calculate_length(&s1); // the function borrows s1's reference.
    // s1 remains the String owner

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

🔧 Complete Runnable Example

// Demonstrates borrowing vs ownership transfer

fn calculate_length(s: &String) -> usize {
    s.len() // Can read the string
    // s.push_str(" world"); // ❌ This would cause a compile error!
}

fn take_ownership(s: String) -> usize {
    s.len() // Takes ownership, original becomes invalid
}

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

    // ✅ Borrowing - s1 remains valid
    let len1 = calculate_length(&s1);
    println!("Length: {len1}");
    println!("Original string: {s1}"); // Still works!

    // ✅ Can borrow multiple times
    let len2 = calculate_length(&s1);
    println!("Length again: {len2}");

    // ❌ If we did this instead:
    // let len3 = take_ownership(s1); // Moves s1
    // println!("{s1}"); // Would cause compile error!
}

🎯 Key Concepts Explained

1. References Don't Own

fn process_string(s: &String) {
    // s is just pointing to the data
    // When function ends, s disappears but data remains
}

2. Original Owner Stays Responsible

let owner = String::from("data");
process_string(&owner); // owner still responsible for cleanup
// owner gets dropped here when it goes out of scope

3. Multiple Borrows Are OK

let data = String::from("hello");
let len1 = calculate_length(&data);
let len2 = calculate_length(&data); // ✅ Fine!
let len3 = calculate_length(&data); // ✅ Still fine!

⚠️ Common Pitfalls to Avoid

Pitfall 1: Trying to Modify Through Immutable Reference

fn bad_function(s: &String) {
    s.push_str(" world"); // ❌ Compile error!
}

Fix: Use mutable reference if you need to modify:

fn good_function(s: &mut String) {
    s.push_str(" world"); // ✅ Works!
}

Pitfall 2: Confusing Borrowing with Moving

// ❌ This moves ownership:
fn takes_ownership(s: String) -> usize { s.len() }

// ✅ This borrows:
fn borrows_reference(s: &String) -> usize { s.len() }

Pitfall 3: Dangling References

fn bad_example() -> &String {
    let s = String::from("hello");
    &s // ❌ s will be dropped, reference becomes invalid!
}

🚀 Practical Tips

Tip 1: Prefer Borrowing Over Moving

// ✅ Good: Function can be called multiple times
fn process_data(data: &String) { /* ... */ }

// ❌ Less flexible: Can only be called once
fn process_data(data: String) { /* ... */ }

Tip 2: Use &str Instead of &String When Possible

// ✅ More flexible - accepts both String and &str
fn better_function(s: &str) -> usize {
    s.len()
}

// ❌ Less flexible - only accepts String references
fn limited_function(s: &String) -> usize {
    s.len()
}

Tip 3: Think "Lending Library"

  • Owner = Library that owns books
  • Borrower = Person who checks out books
  • Reference = Library card showing which book you borrowed
  • Scope = How long you can keep the book

🧪 Try This Exercise

fn count_words(text: &String) -> usize {
    text.split_whitespace().count()
}

fn count_chars(text: &String) -> usize {
    text.chars().count()
}

fn main() {
    let message = String::from("Rust is awesome!");

    // Try these function calls:
    let word_count = count_words(&message);
    let char_count = count_chars(&message);

    println!("Message: {message}");
    println!("Words: {word_count}, Characters: {char_count}");
}

Run it with:

rustc borrowing_practice.rs && ./borrowing_practice

🎉 Summary

Borrowing in Rust is like a respectful lending system:

  • You can lend your data without giving up ownership
  • Multiple people can borrow and read simultaneously
  • The original owner remains responsible for cleanup
  • Everyone follows the rules - no stealing or damaging!

This system prevents memory bugs while allowing efficient data sharing. Pretty neat, right? 🚀

📊 Flow Chart for borrow_practice.rs

Here's a visual representation of how the borrowing works:

┌─────────────────────────────────────────────────────────────────┐
│                           PROGRAM START                         │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                    main() function                              │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ let message = String::from("Rust is awesome!");        │   │
│  │ 📚 OWNER: message owns "Rust is awesome!"              │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│              First Function Call                                │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ let word_count = count_words(&message);                 │   │
│  │ 🤝 BORROW: &message creates reference                   │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                count_words(&String) function                    │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ fn count_words(text: &String) -> usize {               │   │
│  │   text.split_whitespace().count()                      │   │
│  │ }                                                       │   │
│  │ 👀 READ-ONLY: Can read but not modify                   │   │
│  │ 📊 RESULT: Returns 3 (word count)                       │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                  Borrow Ends                                    │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 🔄 RETURN: Reference goes out of scope                  │   │
│  │ ✅ OWNER: message still owns the String                 │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│              Second Function Call                               │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ let char_count = count_chars(&message);                 │   │
│  │ 🤝 BORROW: &message creates another reference           │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                count_chars(&String) function                    │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ fn count_chars(text: &String) -> usize {               │   │
│  │   text.chars().count()                                 │   │
│  │ }                                                       │   │
│  │ 👀 READ-ONLY: Can read but not modify                   │   │
│  │ 📊 RESULT: Returns 16 (character count)                 │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                  Borrow Ends Again                              │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 🔄 RETURN: Reference goes out of scope                  │   │
│  │ ✅ OWNER: message still owns the String                 │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                    Print Results                                │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ println!("Message: {message}");                         │   │
│  │ println!("Words: {word_count}, Characters: {char_count}");│   │
│  │ ✅ VALID: message is still accessible                   │   │
│  │ 📤 OUTPUT: Message: Rust is awesome!                    │   │
│  │           Words: 3, Characters: 16                     │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                    End of main()                                │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 🗑️ CLEANUP: message goes out of scope                   │   │
│  │ 💾 MEMORY: String "Rust is awesome!" is deallocated     │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                      PROGRAM END                                │
└─────────────────────────────────────────────────────────────────┘

🔍 Key Ownership Flow Points:

1. Ownership Timeline

Time 0: message owns "Rust is awesome!"
Time 1: count_words borrows &message (message still owns)
Time 2: count_words returns, borrow ends (message still owns)
Time 3: count_chars borrows &message (message still owns)
Time 4: count_chars returns, borrow ends (message still owns)
Time 5: println! uses message (message still owns)
Time 6: main() ends, message dropped (ownership ends)

2. Memory State Diagram

Stack Memory:
┌─────────────────┐
│ main() frame    │
│ ┌─────────────┐ │
│ │ message     │ │ ──┐
│ │ word_count  │ │   │
│ │ char_count  │ │   │
│ └─────────────┘ │   │
└─────────────────┘   │
                      │
Heap Memory:          │
┌─────────────────┐   │
│ "Rust is       │ ◄─┘
│  awesome!"     │
│ (String data)  │
└─────────────────┘

3. Borrowing Rules Applied

  • Multiple immutable borrows: Both functions can borrow simultaneously
  • No ownership transfer: Original owner (message) retains control
  • Scope safety: References are valid only during function calls
  • Memory safety: No dangling pointers or use-after-free

This flow chart shows how Rust's borrowing system allows safe, efficient access to data without transferring ownership! 🦀