Ownership and Borrowing in Rust: Memory Safety Without Garbage Collection¶
Introduction to Rust's Memory Management¶
Rust solves one of the most challenging problems in systems programming: finding the balance between speed, safety, concurrency, and portability. While languages like C and C++ are blazingly fast but unsafe, and languages like Python provide garbage collection but with performance overhead, Rust achieves memory safety without a garbage collector through its unique ownership system.
Why Ownership Exists¶
Traditional approaches to memory management have significant drawbacks:
- Manual Memory Management (C/C++): Developers control memory allocation and deallocation, leading to bugs like:
- Double-free errors
- Memory leaks
-
Use-after-free vulnerabilities
-
Garbage Collection (Java, Python): Automatic memory management that stops program execution to clean up memory, causing:
- Performance overhead
- Unpredictable pauses
- Not suitable for real-time systems
Rust's ownership system provides memory safety at compile time, eliminating these issues without runtime overhead.
The Three Rules of Ownership¶
Understanding ownership requires memorizing these fundamental rules:
- Each value in Rust has an owner
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped
Rule 1: Each Value Has an Owner¶
Here, s1 owns the String value "rust". The ownership relationship is clear and unambiguous.
Rule 2: Only One Owner at a Time¶
fn main() {
let s1 = String::from("rust");
let s2 = s1; // Ownership transfers from s1 to s2
// println!("{}", s1); // ❌ This would cause a compile error
println!("{}", s2); // ✅ This works - s2 is now the owner
}
When we assign s1 to s2, ownership is moved (not copied). s1 is no longer valid after this point.
Rule 3: Values are Dropped When Owner Goes Out of Scope¶
fn main() {
{
let s1 = String::from("rust");
// s1 is valid here
} // s1 goes out of scope and is dropped here
// s1 is no longer accessible
}
Understanding References and Borrowing¶
Borrowing allows you to use values without taking ownership. This is accomplished through references.
Creating References¶
fn main() {
let s1 = String::from("rust");
let len = calculate_length(&s1); // Pass a reference, not the value
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() // We can read the data without owning it
}
The & symbol creates a reference. The function calculate_length borrows the string rather than taking ownership.
Immutable vs Mutable References¶
By default, references are immutable:
fn main() {
let mut s = String::from("hello");
// Immutable reference
let r1 = &s;
println!("{}", r1);
// Mutable reference (requires mut keyword for both variable and reference)
let r2 = &mut s;
r2.push_str(" world");
println!("{}", r2);
}
The Borrowing Rules¶
Rust enforces these rules at compile time:
- You can have either one mutable reference OR any number of immutable references
- References must always be valid
fn main() {
let mut s = String::from("hello");
let r1 = &s; // ✅ Immutable reference
let r2 = &s; // ✅ Another immutable reference
// let r3 = &mut s; // ❌ Cannot have mutable reference while immutable refs exist
println!("{} {}", r1, r2);
let r3 = &mut s; // ✅ Now this works - immutable references are no longer used
r3.push_str(" world");
}
Practical Example: Bank Account¶
Here's a practical example showing how borrowing prevents data races:
struct BankAccount {
owner: String,
balance: f64,
}
impl BankAccount {
fn withdraw(&mut self, amount: f64) {
println!("Withdrawing {} from account owned by {}", amount, self.owner);
self.balance -= amount;
}
fn check_balance(&self) {
println!("Account owned by {} has a balance of {}", self.owner, self.balance);
}
}
fn main() {
let mut account = BankAccount {
owner: "Alice".to_string(),
balance: 150.55,
};
// Immutable borrow to check balance
account.check_balance();
// Mutable borrow to withdraw money
account.withdraw(45.50);
// Check balance again
account.check_balance();
}
Each borrow occurs in its own scope, preventing simultaneous mutable and immutable access.
Memory Safety Benefits¶
Rust's ownership system prevents common memory safety issues:
Dangling Pointers¶
// This won't compile - Rust prevents dangling references
fn dangle() -> &String { // ❌ Missing lifetime specifier
let s = String::from("hello");
&s // s is dropped when function ends, making this reference invalid
}
Data Races¶
The borrowing rules prevent data races at compile time by ensuring that mutable access is exclusive.
Use After Free¶
Once a value is moved, the original variable becomes invalid, preventing use-after-free bugs.
Key Takeaways¶
- Ownership provides memory safety without garbage collection
- Moving transfers ownership; borrowing provides temporary access
- References allow sharing data without compromising safety
- Compile-time checks prevent entire classes of memory bugs
- The borrowing checker enforces these rules automatically
This system makes Rust uniquely positioned for systems programming where both performance and safety are critical. The compiler catches memory safety violations before your code ever runs, eliminating a major source of security vulnerabilities and crashes in systems software.
Understanding ownership and borrowing is fundamental to writing effective Rust code. While it may seem restrictive at first, these constraints guide you toward writing safer, more concurrent code that performs excellently in production environments.