rust-referencias-borrowing

References and Borrowing in Rust: rules and loans

  • 6 min

A reference is a way to use a value without taking ownership of it.

In the previous article, we saw that if we pass a complex variable (like a String) to a function, we transfer ownership to it. The original variable dies and we cannot use it again.

This is very safe, but terribly inconvenient. Imagine having to return the variable at the end of every function just to be able to keep using it:

// What we would have to do WITHOUT references (How horrible!)
fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1); // We have to return s1 to not lose it
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length) // Returns the String and the length
}
Copied!

Wouldn’t it be better to be able to “lend” the variable to a function so it can read it, without giving away ownership? That is exactly what Borrowing is.

Immutable References (&T)

A reference is like a pointer in C++; it is a memory address that allows us to access data owned by another variable. In Rust, we create references using the ampersand symbol (&).

The act of creating a reference is called Borrowing.

fn main() {
    let s1 = String::from("hello");

    // We pass &s1 (a reference), not s1 (the value)
    let len = calculate_length(&s1);

    // s1 is still valid here! Because we only "lent" it.
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, 's' goes out of scope. But since it does NOT have ownership, nothing happens.
  // The original String is NOT dropped.
Copied!

Visually, &s1 creates a pointer that points to the structure of s1 (which in turn points to the Heap). We are not the owner of the data; we only have permission to look at it.

By default, references are immutable. If you try to modify something borrowed with &, the compiler will give you an error. “Look, but don’t touch.”

Mutable References (&mut T)

What if we want to lend something so someone can modify it? (For example, a function that adds text to a String).

We need two things:

  1. The original variable must be mutable (mut).
  2. Pass a mutable reference using &mut.
fn main() {
    let mut s = String::from("hello");

    change(&mut s); // We pass a mutable reference

    println!("{}", s); // Prints "hello world"
}

fn change(some_string: &mut String) {
    some_string.push_str(" world");
}
Copied!

Perfect! We have modified the data without taking ownership of it. From here on, the Borrow Checker ensures that references do not overlap in an incompatible way.

The Rules of Borrowing

To guarantee memory safety and prevent incompatible mutable aliasing, Rust imposes two rules on references. These same rules also prevent data races when sharing data between threads through safe APIs.

Rule 1: The Rule of Coexistence

While references are still in use, you can have one of the following options for the same value, but not both:

  • Any number of immutable references (&T).
  • Exactly one mutable reference (&mut T).

Think of it like a Google Docs document:

  • Read Mode: Many people can read the document at the same time without a problem.
  • Edit Mode: If someone is writing, no one else should be reading or writing at the same time to avoid chaos.

Failed attempt: two mutables at the same time

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s; // ❌ ERROR: cannot borrow `s` as mutable more than once at a time

println!("{}, {}", r1, r2);
Copied!

Rust prevents this aliasing. In concurrent code, allowing two unsynchronized accesses to write to the same memory could produce a data race.

Failed attempt: Mixing reading and writing

let mut s = String::from("hello");

let r1 = &s; // Reading
let r2 = &s; // Reading
let r3 = &mut s; // ❌ ERROR: cannot borrow `s` as mutable because it is also borrowed as immutable

println!("{}, {}, {}", r1, r2, r3);
Copied!

If r1 is reading, it does not expect the data to change under its feet. r3 could change the content or even resize the string (invalidating the memory), leaving r1 pointing to nothing. Rust forbids this.

The Scope of Each Loan

These rules apply to the lifetime of the reference. If a reference is no longer used, we can create another new one.

A block allows the explicit termination of a reference’s usage before creating another:

let mut s = String::from("hello");

{
    let r1 = &mut s;
    // r1 does its work here...
} // r1 dies here. The "write lock" is released.

let r2 = &mut s; // ✅ Now we can create another mutable reference.
Copied!

Furthermore, the modern Rust compiler is smart (thanks to a feature called Non-Lexical Lifetimes). It knows when you stop using a reference.

let mut s = String::from("hello");

let r1 = &s;
println!("{}", r1);
// The compiler sees that r1 is NOT used below.
// Therefore, its "loan" ends here implicitly.

let r2 = &mut s; // ✅ This works because r1 is no longer in the way.
r2.push_str(" world");
Copied!

Rule 2: References are always valid (No Dangling References)

In languages like C++, it is easy to create a pointer that points to memory that has already been freed (Dangling Pointer).

// Example in C++ (Dangerous)
string* create() {
    string s = "hello";
    return &s; // 💀 Returns a reference to a local variable that is about to die
}
Copied!

If you try to do this in Rust:

fn create<'a>() -> &'a String {
    let s = String::from("hello");
    &s // ❌ ERROR: returns a reference to data owned by the current function
}
Copied!

Rust stops you. The function promises a reference with a lifetime chosen by the caller, but s will be destroyed when the call ends. No valid lifetime exists for that reference.

The solution in this case is to return the String directly (transfer Ownership), not the reference.