9.1 Shared References: &T, Read-Only Access
Alright, let’s get our hands dirty with references. You’ve met &T—the ampersand-type. It’s Rust’s way of giving you a key to a room without handing you the deed to the building. You can look, but you can’t knock down the walls. This is shared, read-only access, and it’s the bedrock of Rust’s memory safety without a garbage collector.
Think of a &T as a library pass. The library (the data) exists in one place, managed by someone else (the owner). You get a pass (&T) that lets you go in and read any book you want. But you can’t burn a book, and you certainly can’t decide the library should now be a nightclub. Most importantly, the library’s management knows exactly how many passes are out there at any given time, ensuring it’s never suddenly overcrowded or torn down while someone’s still inside.
The Immutable Rules of the Game
The core principle of a shared reference is immutability. Through a &T, the data it points to is frozen. You cannot modify it. Full stop. This isn’t a suggestion; it’s a compiler-enforced guarantee.
let mut answer = 42; // Mutable owner
let reader: &i32 = &answer; // Create a shared reference
// This is perfectly fine:
println!("The reader sees: {}", reader);
// This is an absolute felony. The compiler will stop you dead:
// *reader += 1; // ERROR: cannot assign to `*reader` which is behind a `&` reference
Why this tyranny? It prevents data races. If multiple parts of your code hold read-only access to the same value, they can all read it simultaneously without any risk of one changing it unexpectedly from under the others. It’s the concurrency model’s best friend.
The Borrow Checker: Your Overzealous Librarian
When you create a reference (&something), you are borrowing it. The brilliant, slightly pedantic librarian (the borrow checker) now starts tracking this loan. Its primary rule: You can have as many immutable borrows (&T) as you want, as long as no mutable borrow (&mut T) exists simultaneously.
This rule is the magic. Let’s see it in action.
fn main() {
let book = String::from("The Rust Programming Language");
let borrow1 = &book; // First shared borrow
let borrow2 = &book; // Second shared borrow. Totally fine.
println!("Borrow1: {}, Borrow2: {}", borrow1, borrow2);
// Both are still in scope here.
// let mut_borrow = &mut book; // ERROR! Cannot borrow `book` as mutable because it is also borrowed as immutable.
// The librarian sees borrow1 and borrow2 are still "in use" (in scope) and protects the data.
} // borrow1 and borrow2 go out of scope here. The loan is returned.
// Now, and only now, could we create a mutable borrow if we wanted to.
The moment the last use of the immutable references is done, the borrow is considered “returned,” and the data is free to be mutated again. The compiler isn’t just tracking creation; it’s tracking the entire lifetime of the borrow.
It’s Not a Pointer, It’s a Promise (Mostly)
Under the hood, a &T is often a pointer. But calling it that sells it short. It’s a pointer plus a contract. The contract states that the data it points to is valid for the entire lifetime of the reference. The borrow checker exists to prove this contract is upheld. This is why you can’t return a reference to data created inside a function—the data vanishes at the }, leaving the reference pointing to garbage and violating the contract.
fn invalid_reference() -> &String {
let local_data = String::from("poof!");
&local_data // ERROR: `local_data` does not live long enough.
} // `local_data` is dropped here, making the reference invalid.
The Case of the Misleading Mut
Here’s a classic head-scratcher that trips everyone up.
let mut owner = 50;
let reader = &owner; // Type is &i32
// Later...
owner = 100; // Wait, isn't `owner` mutated?!
println!("Reader is: {}", reader); // Probably prints 50? 100?
Is this allowed? It feels like it should break the rules. The trick is in the timing. The mutation owner = 100; occurs after the last use of the borrowed reference (reader). The borrow checker sees that reader is never used again after the mutation, so it allows it. The reference reader still points to the old memory location, which now holds the value 100. This is safe, if a bit confusing. If you used reader after the mutation, it would be an error.
let mut owner = 50;
let reader = &owner;
owner = 100; // This is only allowed because...
// println!("{}", reader); // ...if this line existed, it would be an ERROR!
The designers made a pragmatic choice here. It would be overly restrictive to prevent all mutations of the original owner; they only prevent them while the reference is actively in use. It’s a good reminder that the borrow checker’s logic is deeply tied to the lifetime of the reference, not just its existence.