Borrowing in Depth
In the previous section, we introduced Rust’s ownership system and the concept of borrowing. Now, let’s dive deeper into borrowing and explore its nuances.
Quick Recap: What is Borrowing?
Borrowing in Rust is the ability to reference a value without taking ownership of it. It’s similar to how in JavaScript you can pass an object to a function which can read or modify it without affecting whether the caller still has access to that object.
fn main() { let s = String::from("hello");
// Immutable borrow - s is passed by reference let len = calculate_length(&s);
println!("The length of '{}' is {}.", s, len);}
fn calculate_length(s: &String) -> usize { s.len()}
Types of References
Rust has two types of references:
- Immutable references (
&T
) - Allow reading but not modifying - Mutable references (
&mut T
) - Allow both reading and modifying
Let’s explore each in detail.
Immutable References
Immutable references allow you to read data but not modify it:
fn main() { let s = String::from("hello");
let r1 = &s; // First immutable reference let r2 = &s; // Second immutable reference
println!("{} and {}", r1, r2);}
You can have multiple immutable references to the same data at the same time. This is similar to multiple variables in JavaScript referencing the same object.
Mutable References
Mutable references allow you to modify the borrowed data:
fn main() { let mut s = String::from("hello");
let r1 = &mut s; // Mutable reference r1.push_str(", world");
println!("{}", r1);}
The Borrowing Rules
Rust enforces the following rules for borrowing:
-
At any given time, you can have either:
- One mutable reference, OR
- Any number of immutable references
-
References must always be valid (no dangling references)
These rules prevent data races at compile time. A data race occurs when:
- Two or more pointers access the same data simultaneously
- At least one of the pointers is being used to write to the data
- There’s no mechanism to synchronize the access
Example: Preventing Data Races
This code will not compile because it violates rule #1:
fn main() { let mut s = String::from("hello");
let r1 = &s; // Immutable borrow let r2 = &s; // Another immutable borrow - this is fine let r3 = &mut s; // ❌ Error: cannot borrow `s` as mutable because it is also borrowed as immutable
println!("{}, {}, and {}", r1, r2, r3);}
Non-Lexical Lifetimes (NLL)
Rust’s borrow checker is smart enough to understand when a reference is no longer needed:
fn main() { let mut s = String::from("hello");
let r1 = &s; let r2 = &s; println!("{} and {}", r1, r2); // r1 and r2 are no longer used after this point
let r3 = &mut s; // ✅ This is fine now println!("{}", r3);}
This is called Non-Lexical Lifetimes (NLL) - the compiler understands that the immutable borrows are no longer used after the first println!
, so it allows the mutable borrow.
Borrowing and Functions
When you pass a reference to a function, the function borrows the value:
fn main() { let mut s = String::from("hello");
add_world(&mut s); // Pass mutable reference println!("{}", s); // Prints "hello, world"}
fn add_world(s: &mut String) { s.push_str(", world");}
The function add_world
borrows s
mutably, modifies it, and then the borrow ends when the function returns.
Comparing to JavaScript
In JavaScript, all objects are passed by reference implicitly:
function addWorld(s) { s.value += ", world";}
const myString = { value: "hello" };addWorld(myString);console.log(myString.value); // "hello, world"
But there’s a crucial difference: JavaScript doesn’t prevent data races at compile time. This code will run without errors but could cause bugs:
// JavaScript doesn't prevent this potential race conditionconst data = { value: 10 };
// These could run concurrently in a web appfunction increment() { data.value += 1;}
function double() { data.value *= 2;}
The Ref Pattern in JavaScript
Some JavaScript libraries use a pattern that’s conceptually similar to Rust’s references:
// React's useRef hook creates a mutable referencefunction Counter() { const countRef = useRef(0);
function handleClick() { countRef.current += 1; console.log(`You clicked ${countRef.current} times`); }
return <button onClick={handleClick}>Click me</button>;}
Borrowing and Iterators
When working with collections, borrowing becomes especially important:
fn main() { let mut v = vec![1, 2, 3, 4, 5];
// Immutable borrow for iteration for i in &v { println!("{}", i); }
// Mutable borrow for modifying during iteration for i in &mut v { *i += 50; // Dereference to modify the value }
println!("{:?}", v); // [51, 52, 53, 54, 55]}
The Borrow Checker
Rust’s borrow checker is the compiler component that enforces these rules. It analyzes the scope of all borrows and ensures they don’t violate any rules.
If the borrow checker can prove that your code follows all the borrowing rules, it compiles successfully. Otherwise, it rejects your code with an error message.
Interior Mutability
Sometimes you need to modify a value even when you only have an immutable reference. Rust provides interior mutability patterns for these cases:
use std::cell::RefCell;
fn main() { let data = RefCell::new(5);
// We can modify the value through an immutable reference let borrowed = &data; *borrowed.borrow_mut() += 1;
println!("Value: {}", *borrowed.borrow()); // Value: 6}
Interior mutability is a relatively advanced topic, but it’s similar to how in JavaScript you can have a const
object but still modify its properties:
const user = { name: "Alice", score: 0};
// The binding is const, but we can modify propertiesuser.score += 10;
Common Borrowing Patterns
Splitting Borrows
You can borrow different parts of a data structure simultaneously:
fn main() { let mut s = String::from("hello world");
// Split the string into two parts let hello = &s[0..5]; // Immutable borrow of first 5 chars let world = &mut s[6..11]; // Mutable borrow of remaining chars
// This is allowed because the borrows don't overlap world.make_ascii_uppercase();
println!("{} {}", hello, world); // "hello WORLD"}
Self-Referential Structs
Creating structures that contain references to themselves is challenging in Rust. This is where you might need more advanced patterns like Rc
and RefCell
or create a different data structure.
Conclusion
Borrowing is a core concept in Rust that enables:
- Memory safety without a garbage collector
- Concurrency without data races
- Efficient passing of data without unnecessary copying
While borrowing rules might seem restrictive coming from JavaScript, they prevent entire classes of bugs that would only appear at runtime in JavaScript.
In the next section, we’ll explore Lifetimes, which are Rust’s way of ensuring that references are always valid.