Step 04 of 17
References & borrowing
Lend values with & and &mut, follow the borrowing rules, and use slices, String and &str correctly in function signatures.
Moving a value into every function that needs to look at it would be unbearable. Instead, you borrow it: you hand out a reference, and ownership stays where it was. This lesson covers shared and mutable references, the one rule the borrow checker enforces, why that rule prevents real bugs, and slices — including the String versus &str question every Rust beginner asks.
Shared references
A reference, written &T, points at a value without owning it. Creating one is called borrowing.
fn length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it doesn't own anything, so nothing is dropped
fn main() {
let name = String::from("Ferris");
let n = length(&name); // lend name to the function
println!("{name} has {n} letters"); // name is still ours
}&name creates a reference. The function takes &String and reads through it. When the function returns, the reference disappears, and name is untouched.
You don't need to write *s to read through a reference most of the time: method calls and field access auto-dereference, so s.len() works on a &String as it would on a String. You do write * explicitly when you assign through a mutable reference or compare a reference with a plain value.
Shared references are read-only:
fn add_suffix(s: &String) {
s.push_str("!"); // error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
}Mutable references
To modify a borrowed value, take a mutable reference, &mut T. The variable itself must be declared mut too:
fn add_suffix(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut greeting = String::from("hello");
add_suffix(&mut greeting);
println!("{greeting}"); // hello!
let mut count = 0;
let r = &mut count;
*r += 1; // explicit dereference to assign through the reference
println!("{count}"); // 1
}The &mut at the call site is deliberate: when you read add_suffix(&mut greeting), you know greeting may change. Code that only borrows with & can't change it.
The borrowing rule
At any moment, a value can have either:
- any number of shared references (
&T), or - exactly one mutable reference (
&mut T),
but not both. And every reference must be valid: it can't outlive the value it points to.
Put shortly: aliasing XOR mutation. Many readers or one writer, never a writer alongside anyone else.
fn main() {
let mut scores = vec![10, 20, 30];
let first = &scores[0]; // shared borrow of scores
scores.push(40); // needs &mut scores
println!("{first}"); // shared borrow still in use
}error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &scores[0];
| ------ immutable borrow occurs here
4 | scores.push(40);
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{first}");
| ------- immutable borrow later used hereThis isn't the compiler being fussy. push may need more capacity, in which case Vec allocates a new buffer, copies the elements and frees the old one. first would then point into freed memory. In C++ this exact bug (iterator invalidation) is a classic source of crashes and exploits. Rust rejects it at compile time.
The same rule prevents data races: a data race needs two accesses to the same memory with at least one write, and "one writer xor many readers" forbids exactly that. You'll see how this extends to threads in the concurrency lesson.
Borrows end at their last use
A reference's borrow lasts until the reference is last used, not until the end of the block. This is called non-lexical lifetimes, and it's why the following compiles:
fn main() {
let mut scores = vec![10, 20, 30];
let first = &scores[0];
println!("{first}"); // last use of first: the shared borrow ends here
scores.push(40); // fine: no other borrow is live
println!("{scores:?}");
}Often, fixing a borrow error just means reordering: finish reading before you start writing, or copy the value out (let first = scores[0]; copies the i32, so no borrow remains at all).
References can't dangle
fn dangle() -> &String {
let s = String::from("temporary");
&s
} // s is dropped here, so the reference would point at freed memoryThis doesn't compile; the compiler reports a missing lifetime specifier and suggests returning the String itself. The fix is to return an owned value and let ownership move to the caller:
fn no_dangle() -> String {
String::from("temporary")
}How the compiler reasons about how long references live is the subject of the lifetimes lesson. For now: a reference can never outlive its owner.
Slices
A slice is a reference to a contiguous part of a collection. It's a "fat pointer": a pointer plus a length. &[T] is a slice of any array or vector of T:
fn sum(values: &[i32]) -> i32 {
let mut total = 0;
for v in values {
total += v;
}
total
}
fn main() {
let array = [1, 2, 3, 4, 5];
let vector = vec![10, 20, 30];
println!("{}", sum(&array)); // whole array
println!("{}", sum(&vector)); // whole Vec: &Vec<i32> coerces to &[i32]
println!("{}", sum(&array[1..4])); // 2 + 3 + 4 = 9
}Range syntax picks the part: &v[1..4] is indexes 1, 2, 3; &v[..2] is the first two; &v[2..] is from index 2 to the end. Out-of-range slices panic, like indexing.
Taking &[T] instead of &Vec<T> makes a function more useful: it accepts arrays, vectors and sub-ranges of either, at no cost.
String and &str
Rust has two main string types, and the difference is ownership.
String | &str | |
|---|---|---|
| What | An owned, growable UTF-8 buffer on the heap | A borrowed slice of UTF-8 bytes |
| Owns its data | Yes, freed on drop | No, points into someone else's data |
| Can grow | Yes (push_str, push) | No |
| Created by | String::from("…"), "…".to_string(), format! | String literals, &my_string, &my_string[a..b] |
A string literal like "hello" is a &'static str: a slice pointing into the program binary, valid for the whole run.
Take &str in function parameters unless you need to own or modify the string. A &String coerces to &str automatically (this is called deref coercion), so callers can pass either:
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
let owned = String::from("hello world");
println!("{}", first_word(&owned)); // &String -> &str
println!("{}", first_word("rust lang")); // literal is already &str
}Return String when you build a new string; you can't return a reference to something you created inside the function.
Because strings are UTF-8, a character can take one to four bytes. That's why s[0] doesn't compile: indexing by byte could split a character. Slice by byte ranges you know are on character boundaries (like those find returns), or iterate with s.chars():
fn main() {
let s = "héllo";
println!("{}", s.len()); // 6 bytes: é is two bytes
println!("{}", s.chars().count()); // 5 characters
println!("{:?}", s.chars().nth(1)); // Some('é')
// &s[0..2] would panic: byte 2 is in the middle of 'é'
}Practice
- Write
fn longest_word(text: &str) -> &strthat returns the longest whitespace-separated word (usetext.split_whitespace()). If two words tie, return the first. Call it with both a literal and a&String. - Write
fn normalize(names: &mut Vec<String>)that trims each name and converts it to lower case in place, then removes empty entries (look atVec::retain). - Explain why this doesn't compile, then fix it two different ways:
fn main() {
let mut names = vec![String::from("ana")];
let first = &names[0];
names.push(String::from("bo"));
println!("{first}");
}Show solution
fn longest_word(text: &str) -> &str {
let mut best = "";
for word in text.split_whitespace() {
if word.len() > best.len() {
best = word;
}
}
best
}
fn main() {
let owned = String::from("the quick brown fox");
println!("{}", longest_word(&owned)); // quick
println!("{}", longest_word("borrow checker")); // checker
}The returned &str points into the input, which is why the function can return a reference without owning anything. len() counts bytes; for non-ASCII text, compare chars().count() instead.
fn normalize(names: &mut Vec<String>) {
for name in names.iter_mut() {
*name = name.trim().to_lowercase();
}
names.retain(|n| !n.is_empty());
}iter_mut yields &mut String, so *name = … replaces each string in place. retain keeps elements for which the closure returns true.
firstis a shared borrow ofnames, and it's used afterpush, which needs a mutable borrow.pushcan reallocate the buffer, leavingfirstdangling.
Fix A: finish using first before pushing.
let first = &names[0];
println!("{first}");
names.push(String::from("bo"));Fix B: take an owned copy, so no borrow remains.
let first = names[0].clone();
names.push(String::from("bo"));
println!("{first}");Key takeaways
&Tborrows a value read-only;&mut Tborrows it with permission to change it. Ownership doesn't move.- The rule: many shared references or one mutable reference, never both at once. It rules out iterator invalidation and data races.
- A borrow lasts until its last use, so reordering code often fixes borrow errors.
- References can never outlive what they point to; return owned values from functions that create them.
- Take
&strand&[T]in parameters; returnStringandVec<T>when you build something new.
Check yourself
Quiz
0/4 correct · 4 left
1.Which signature is the most flexible for a function that only reads a list of numbers?
2.Why does Rust reject holding &v[0] across a call to v.push(…)?
3.let s = String::from("día"); What is s.len()?
4.let mut x = 5; let a = &x; println!("{a}"); let b = &mut x; *b += 1; — does it compile?
Concepts in this lesson
One-page notes on each idea, for review or a quick lookup later.
Finished reading?
Mark it complete to fill in your roadmap.