Borrowing and references
Lending a value instead of giving it away: &T to read, &mut T to change, the one rule that keeps them safe, and slices — a view into part of a collection.
Lend, do not give
A reference is a borrowed view of a value: &title is a &String, and the function that takes it can read the value without owning it. Nothing is copied, nothing is moved, and the owner keeps its value when the call returns.
Functions that only read should take &str rather than &String: a &str accepts both a borrowed String and a string literal, so every caller can pass what it already has. If the parameter type is &String, the caller has to borrow a String specifically — narrower than it needs to be.
A &mut passed to a function is reborrowed rather than consumed: after add_badge(&mut account) returns, account is still usable and can be borrowed again. The compiler inserts that reborrow at the call site, which is why a loop can pass the same &mut to a helper on every iteration without the reference being moved away, and why a method taking &mut self can be called repeatedly on one variable.
The rule that keeps it safe
Adding mut to a reference makes it a changeable view: &mut count. And now the rule: at any moment, a value can have either any number of readers (&T) or exactly one writer (&mut T) — never both. The compiler enforces that, everywhere, including across threads.
That one rule is why Rust has no data races. If two parts of a program could change a value at once, one of them would have needed a mutable reference the other had, and the compiler refuses to build it. The error messages that follow this rule — "cannot borrow as mutable because it is also borrowed as immutable" — are the strictness showing its working.
Slices
A slice is a borrowed view of a run of items: &numbers[1..3] is a &[i32], and &text[0..4] is a &str. It is a pointer and a length — it does not copy anything, so slicing a huge list to look at the middle costs nothing.
A function that takes &[f64] or &str works with any array, vector or string a caller has, which is why you will see those types in signatures all over idiomatic Rust: the widest useful view, and no ownership taken.
Read, change, and look inside
fn describe(text: &str) -> usize {
text.len()
}
fn add_badge(name: &mut String) {
name.push_str(" (verified)");
}
fn total(prices: &[f64]) -> f64 {
let mut sum = 0.0;
for price in prices {
sum += price;
}
sum
}
fn main() {
let mut account = String::from("ada");
add_badge(&mut account);
println!("{} has {} characters", account, describe(&account));
let prices = [19.5, 4.0, 12.25];
let first_two = &prices[0..2];
println!("first two: {:?} — total: {:.2}", first_two, total(&prices));
}Lend without losing
Run it, then try to break the rule: make a second mutable borrow while the first is still in use. Read the compiler's message, then move the first borrow into its own scope and watch it build again.
Two immutable borrows can exist at once, so reading is never blocked by other readers. When a mutable borrow ends, readers are free again — which is why a short &mut in a small scope is usually the fix for a borrow error, not a rewrite.
A borrow ends at its last use
The compiler tracks each borrow until the point where it is last used, not to the end of the block. So this builds: borrow a value, print it, then take a &mut to the same value — the read is over, so the writer is free. The older rule, "a borrow lives to the end of the scope", was stricter than the language needed, and the relaxation is called non-lexical lifetimes.
That is why the first fix for a borrow error is usually a smaller scope, not a redesign. Move the read and the write apart, or wrap one borrow in braces, and the two stop overlapping. If they genuinely need to overlap, the data wants to be split into two values or copied.
Slices are ranges into memory
A slice is a pointer and a length, and the range you write picks the part. &numbers[1..3] gives items 1 and 2, not item 3 — the end is exclusive, matching how lengths are counted. &numbers[..2] starts at the beginning, &numbers[2..] runs to the end, and &numbers[..] is the whole thing.
Text slices index bytes, so &text[0..4] panics if byte 4 lands inside a character. Use char_indices() when you need to cut a string safely, or build a new String from chars(). The same rule explains why first_word can return &str without copying: it points into the original text, and the compiler makes sure the text outlives it.
Three borrow mistakes to recognise. Holding a &mut while reading through another name is the classic conflict. Taking &mut of a field while a method borrows the whole struct is the same rule one level down. And a slice that outlives its collection — returned from a function that built the collection locally — is a dangling view the compiler refuses to build.
Try these next
- Write
last_word(text: &str) -> &strby searching backwards for the last space. - Change a function that takes
&Stringto take&str, and call it with both a literal and a borrowedString. - Print a slice of a vector, then push to the vector while the slice is still in use, and read the error.
Borrow a word out of a sentence
Run it and note that `word` is still valid after `sentence` is changed. Then write `last_word` as well, and print the first and last word of the same sentence.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
How many mutable references to one value can exist at the same time?
