Use values without taking ownership through Rust's borrowing system.
In the last lesson, we learned about ownership. Every value has one owner, and when ownership moves, the original variable becomes invalid. That works, but it creates a problem.
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // Have to return s to use it again!
}
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
Look at that function signature. We take a String, do something with it, then have to return both the result AND the original string in a tuple. Otherwise s1 would be gone after the function call.
This is tedious. Imagine needing to pass a string through five functions - you'd be juggling tuples everywhere.
If you find yourself returning ownership just to use a value again, you're fighting the language. References are the idiomatic solution.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Pass a reference
println!("The length of '{s1}' is {len}."); // s1 still valid!
}
fn calculate_length(s: &String) -> usize {
s.len()
}
The & symbol creates a reference to a value. Think of it as pointing to the data without owning it. The function borrows the value, uses it, and when the function ends, the reference goes away - but the original value remains with its original owner.
Here's what's happening under the hood:
Stack Heap
+-------------+ +-------------+ +-------------+
| s (ref) | | s1 (owner) | | |
| ptr --------+--->| ptr --------+--->| "hello" |
+-------------+ | len: 5 | +-------------+
| cap: 5 |
+-------------+
The reference s points to s1, which points to the actual data. When s goes out of scope, nothing happens to the data because s doesn't own it.
A reference is like a pointer, but with a guarantee: it will always point to valid data for its entire lifetime. No null pointers. No dangling pointers. Rust's compiler enforces this.
Creating a reference is called borrowing. The analogy is perfect: if I borrow your book, I don't own it. I have to give it back. While I'm reading it, you still own it, but you can't exactly use it yourself.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("'{s1}' has length {len}");
// s1 is still valid here - we only borrowed it
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but since it doesn't own what it refers to,
// nothing happens
Notice we didn't need to return the string. The reference disappeared, the original stayed. Clean.
What symbol creates an immutable reference to a variable x?
Just as variables are immutable by default, so are references.
fn change(s: &String) {
s.push_str(", world"); // ERROR! Can't modify borrowed value
}
The compiler stops you cold:
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
This makes sense. We're not allowed to modify something we have a reference to.
The opposite of referencing with & is dereferencing with *. You'll see this when working with references directly.
Sometimes you DO need to modify borrowed data. That's where mutable references come in.
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{s}"); // prints "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Two things had to change:
mut - you can't create a mutable reference to an immutable value&mut s instead of &s to create the mutable referenceA common mistake: trying &mut x when x isn't declared with mut. The original variable must be mutable if you want to modify it through a reference.
Now we get to the heart of Rust's safety guarantees. There are exactly two rules for references:
Let's see rule #1 in action:
let mut s = String::from("hello");
let r1 = &s; // ok - immutable reference
let r2 = &s; // ok - another immutable reference
let r3 = &mut s; // ERROR! Can't have mutable while immutable exist
println!("{}, {}, and {}", r1, r2, r3);
The compiler complains:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
Multiple readers? Fine. One writer? Fine. Readers AND a writer at the same time? Absolutely not.
This rule prevents conflicts where one part of your code is reading data while another part is modifying it. The compiler catches these at compile time instead of leaving them as runtime surprises.
Here's a subtlety that trips people up. A reference's scope lasts from where it's introduced to where it's last used - not to the end of the block.
let mut s = String::from("hello");
let r1 = &s; // r1 scope starts
let r2 = &s; // r2 scope starts
println!("{r1} and {r2}");
// r1 and r2 are never used after this point
let r3 = &mut s; // ok! r1 and r2 scopes ended
println!("{r3}");
This compiles because r1 and r2 aren't used after the first println!. The compiler is smart enough to see that their scopes end there, making room for the mutable reference.
If the borrow checker rejects your code, trace where each reference is last used. Often, restructuring your code so references don't overlap fixes the issue.
Let's see what could go wrong without these rules. Imagine we could have a mutable and immutable reference at the same time:
// Hypothetical dangerous code (doesn't compile)
let mut data = vec![1, 2, 3];
let first = &data[0]; // Points to the first element
data.push(4); // This might reallocate the vector!
println!("{first}"); // first might point to freed memory!
If push causes the vector to reallocate, the memory where first points gets freed. first would become a dangling pointer. Accessing it would be undefined behavior - maybe a crash, maybe garbage data, maybe something worse.
Rust's rules make this impossible:
error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable
The compiler catches this at compile time.
Rule #2 says references must always be valid. Rust prevents dangling references at compile time.
fn dangle() -> &String { // Returns a reference to a String
let s = String::from("hello");
&s // Return a reference to s
} // s goes out of scope and is dropped. Danger!
The compiler catches this:
error[E0106]: missing lifetime specifier
--> src/main.rs:1:16
|
1 | fn dangle() -> &String {
| ^ expected named lifetime parameter
The string s is created inside the function. When the function ends, s is dropped. If we could return a reference to it, that reference would point to freed memory.
The solution? Return the value itself, transferring ownership:
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership moves to caller
}
The error message mentioned "lifetime specifier." Lifetimes are Rust's way of tracking how long references are valid. We'll cover them in a future lesson. For now, just know that the compiler uses lifetimes to enforce Rule #2.
Borrowing and references are how you use values without taking ownership. Here are the key points:
Creating References:
&x creates an immutable reference to x&mut x creates a mutable reference to x (requires x to be mut)The Two Rules:
Why It Matters:
Can you have a mutable reference while immutable references exist?
The borrow checker might feel strict at first. But over time, you'll realize it's catching real bugs before your code even runs.
In the next lesson, we'll look at slices - a special kind of reference that lets you reference a portion of a collection rather than the whole thing.