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}.");
}Think of this like lending a book to a friend:
- You own the book (like
s1owns 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
fn calculate_length(s: &String) -> usize {
s.len()
}What's happening:
s: &Stringmeans "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
let s1 = String::from("hello");What's happening:
s1owns the String "hello"- It's responsible for cleaning up the memory when done
- Think: "s1 bought the book and owns it"
let len = calculate_length(&s1);What's happening:
&s1creates a temporary reference to s1's value- The function borrows this reference
s1still owns the original String- Think: "s1 lends the book to the function to read"
println!("The length of '{s1}' is {len}.");What's happening:
s1is still valid and can be used!- The borrow ended when the function returned
- Think: "The book was returned, so s1 can use it again"
// 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}.");
}// 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!
}fn process_string(s: &String) {
// s is just pointing to the data
// When function ends, s disappears but data remains
}let owner = String::from("data");
process_string(&owner); // owner still responsible for cleanup
// owner gets dropped here when it goes out of scopelet data = String::from("hello");
let len1 = calculate_length(&data);
let len2 = calculate_length(&data); // ✅ Fine!
let len3 = calculate_length(&data); // ✅ Still fine!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!
}// ❌ This moves ownership:
fn takes_ownership(s: String) -> usize { s.len() }
// ✅ This borrows:
fn borrows_reference(s: &String) -> usize { s.len() }fn bad_example() -> &String {
let s = String::from("hello");
&s // ❌ s will be dropped, reference becomes invalid!
}// ✅ Good: Function can be called multiple times
fn process_data(data: &String) { /* ... */ }
// ❌ Less flexible: Can only be called once
fn process_data(data: String) { /* ... */ }// ✅ 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()
}- 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
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_practiceBorrowing 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? 🚀
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 │
└─────────────────────────────────────────────────────────────────┘
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)
Stack Memory:
┌─────────────────┐
│ main() frame │
│ ┌─────────────┐ │
│ │ message │ │ ──┐
│ │ word_count │ │ │
│ │ char_count │ │ │
│ └─────────────┘ │ │
└─────────────────┘ │
│
Heap Memory: │
┌─────────────────┐ │
│ "Rust is │ ◄─┘
│ awesome!" │
│ (String data) │
└─────────────────┘
- ✅ 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! 🦀