Advent of Rust 2024: Day 2—References in Rust 🦀

·

4 min read

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 reference

  • attach_message_to_present() can read the message

  • Original gift_message remains unchanged

  • No 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

  1. Immutable References (&):

    • Read-only access to data

    • Multiple immutable references allowed

    • Cannot modify the original value

    • Provides thread-safe sharing of data

  2. 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
OwnershipCreates a new owner for the cloned dataOwnership stays with the original owner
Data DuplicationMakes a deep copy of the dataNo duplication, just a reference
CostPotentially expensive (allocates memory)Cheap (just creates a pointer)
Mutability RulesNot affected by Rust's borrowing rules.Follows borrowing rules (mutable/immutable)
LifespanCloned data has its own independent lifeReference 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 while original is still in scope.

  • Both original and borrowed 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 slice

  • Typically 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

🔗 Rust Book - References and Borrowing

Code Repository

🔗 Link to GitHub Repo - Day 2 Solution