Advent of Rust 2024: Day 2—References in Rust 🦀
Borrowing without ownership
Yesterday, we solved the ownership puzzle using .clone()
. Today, I am introducing a more elegant solution: references!
Original Code (Ownership Transfer)
fn main() {
let gift_message = String::from("Merry Christmas! Enjoy your gift!");
attach_message_to_present(gift_message.clone());
println!("{}", gift_message);
// Uses .clone() to create a copy without losing ownership
}
fn attach_message_to_present(message: String) {
println!("The present now has this message: {}", message);
}
Tests
Test attach message to present
Test no clone should exist
Test logs
Solution
pub fn main() {
let gift_message = String::from("Merry Christmas! Enjoy your gift!");
attach_message_to_present(&gift_message);
println!("{}", gift_message);
}
pub fn attach_message_to_present(message: &String) {
println!("The present now has this message: {}", &message);
}
In our Christmas gift message
&gift_message
creates a referenceattach_message_to_present()
can read the messageOriginal
gift_message
remains unchangedNo memory allocation needed
More efficient than
.clone()
References in Rust? What are they? How different are they from using .clone()
Let’s say you're lending a book to a besto friendo:
You don't give away the entire book
They can read it
You still keep the original book
They promise not to modify the book (by default)
Types of references
Immutable References (
&
):Read-only access to data
Multiple immutable references allowed
Cannot modify the original value
Provides thread-safe sharing of data
Mutable references (
&mut
):Exclusive, modifiable access
Only one mutable reference allowed at a time
Prevents data races at compile-time
Reference Rules
You can have either:
Multiple immutable references
Exactly one mutable reference
References must always be valid
The original data must outlive all references
Memory and Performance
References are essentially pointers, but with compile-time safety checks:
No runtime overhead
Zero-cost abstraction
Prevents common programming errors
Compile-time guaranteed memory safety
.clone()
vs References
Key Differences Between .clone()
and Borrowing
Aspect | .clone() | References |
Ownership | Creates a new owner for the cloned data | Ownership stays with the original owner |
Data Duplication | Makes a deep copy of the data | No duplication, just a reference |
Cost | Potentially expensive (allocates memory) | Cheap (just creates a pointer) |
Mutability Rules | Not affected by Rust's borrowing rules. | Follows borrowing rules (mutable/immutable) |
Lifespan | Cloned data has its own independent life | Reference cannot outlive the original. |
When to Use .clone()
vs Borrowing?
Use .clone()
When:
You need a separate copy of the data
The original and the copy need to have independent lifetimes
You are working with simple data where cloning is inexpensive
Use Borrowing When:
You don't need to modify or take ownership of the data
You want to avoid the cost of copying large data
You're working within the scope of Rust's borrowing rules
What Do Scope and Lifetime Mean in This Context?
Scope
The block of code in which a variable is valid
Defines where a variable can be accessed
Determines the visibility and lifetime of a variable
Lifetime
The time during which a variable's memory is valid
Ensures memory safety
Prevents use of references after the original data is dropped
fn main() {
let original = String::from("Hello"); // original is created and valid here
let borrowed = &original; // Borrow a reference to original
println!("{}", borrowed); // borrowed is used within the lifetime of original
} // original goes out of scope and is dropped here. borrowed stops being valid here too
In this example:
The
borrowed
reference is valid because it’s only used whileoriginal
is still in scope.Both
original
andborrowed
go out of scope at the same time, so there’s no issue.
Bonus: String Slices (&str
)
String
: Owned, growable string type&str
: Immutable reference to a string sliceTypically used for function parameters
More flexible than
&String
Improved Function Signature
fn attach_message_to_present(message: &str) {
println!("The present now has this message: {}", message);
}
This version works with both String
and string literals!
Key Takeaways
References provide borrowed access
Ownership remains with the original variable
Compile-time safety is Rust's superpower
.clone()
is often unnecessary
Recommended Reading
🔗 Rust Book - References and Borrowing