Ownership on its own would make Rust painful. If every function that only wanted to read a String had to take ownership of it and hand it back, simple code would be buried under tuples and reassignments. Borrowing solves this. A reference lets code use a value without owning it, and the compiler's borrow checker proves at compile time that every reference is valid for as long as it is used and that no reference sees a value being changed underneath it.
This lesson covers shared references &T and mutable references &mut T, the central rule "many readers or one writer", how the compiler decides when a borrow ends (non-lexical lifetimes), the dangling references it rejects, slices (&str and &[T]), and how to design function signatures that borrow. You will also learn to read the four borrow-checker errors that beginners hit most: E0502, E0499, E0596 and E0597, along with E0106 and E0515 for returning references. Interviewers ask about the borrowing rules in almost every Rust interview, often with a short snippet and the question "does this compile, and why not?".
References: using a value without owning it
A reference is a pointer to a value owned by someone else, with compile-time guarantees attached. Creating a reference is called borrowing. You create one with &:
fn count_vowels(text: &str) -> usize {
text.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()
}
fn main() {
let name = String::from("Abhishek");
let vowels = count_vowels(&name);
println!("{name} has {vowels} vowels");
}
Output:
Abhishek has 3 vowels
count_vowels takes &str, a reference to string data. &name lends name to the function for the duration of the call. When the function returns, the borrow ends, and main still owns name and can print it. Nothing was moved, nothing was cloned, and nothing was freed when the function ended, because a reference does not own what it points to.
main's stack frame HEAP
name: String
+-----+-----+-----+ +---+---+---+---+---+---+---+---+
| ptr | len | cap |--------> | A | b | h | i | s | h | e | k |
+-----+-----+-----+ +---+---+---+---+---+---+---+---+
^
count_vowels's frame |
text: &str |
+-----+-----+ |
| ptr | len |------------------+ (borrows; owns nothing)
+-----+-----+
Passing &name (a &String) where the function expects &str works because of deref coercion: the compiler automatically turns a &String into a &str pointing at the same bytes. You will see why that matters for function design later in this lesson.
Dereferencing
The * operator dereferences a reference, meaning it follows the pointer to the value. Often you do not need to write it, because Rust automatically dereferences in method calls (r.len()), comparisons between references, and arithmetic operators implemented for references:
fn main() {
let x = 5;
let r = &x;
let rr = &r;
println!("x = {x}, *r = {}, **rr = {}", *r, **rr);
println!("r + 1 = {}", r + 1);
println!("r == &5? {}", r == &5);
let name = String::from("Zoya");
let borrowed = &name;
println!("len = {}", borrowed.len());
}
Output:
x = 5, *r = 5, **rr = 5
r + 1 = 6
r == &5? true
len = 4
rr is a reference to a reference, so it needs ** to reach the number. r + 1 works because the standard library implements addition for &i32 as well as i32. borrowed.len() works because method calls dereference as many times as needed.
Mutable references
A plain &T is a shared reference: it allows reading but not changing the value. To change a borrowed value, you need a mutable reference, &mut T. Both the variable and the borrow must be marked mut:
fn add_exclamation(text: &mut String) {
text.push('!');
}
fn double_all(numbers: &mut [i32]) {
for n in numbers.iter_mut() {
*n *= 2;
}
}
fn main() {
let mut msg = String::from("Well done");
add_exclamation(&mut msg);
println!("{msg}");
let mut nums = vec![1, 2, 3];
double_all(&mut nums);
println!("{nums:?}");
let mut total = 10;
let r = &mut total;
*r += 5;
println!("total = {total}");
}
Output:
Well done!
[2, 4, 6]
total = 15
add_exclamation(&mut msg)lendsmsgmutably, and the function pushes a character onto it.double_alltakes&mut [i32], a mutable slice, and changes each element throughiter_mut(), which yields&mut i32references.*n *= 2dereferences each one to change the number itself.let r = &mut total; *r += 5;changestotalthroughr. To assign through a reference you must write*.
Reading the compiler error: E0596
You cannot mutably borrow something that is not itself mutable. This does not compile:
fn add_exclamation(text: &mut String) {
text.push('!');
}
fn main() {
let msg = String::from("Well done");
add_exclamation(&mut msg);
println!("{msg}");
}
error[E0596]: cannot borrow `msg` as mutable, as it is not declared as mutable
--> mut_of_immut_err.rs:7:21
|
7 | add_exclamation(&mut msg);
| ^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
6 | let mut msg = String::from("Well done");
| +++
Immutability is a property of the binding, and a mutable borrow would be a back door around it. The fix is exactly the help line: declare the variable mut.
The borrowing rules
The borrow checker enforces two rules:
- At any moment, a value may have either any number of shared references (
&T) or exactly one mutable reference (&mut T), but not both. - A reference must never outlive the value it refers to.
People summarise rule 1 as "many readers or one writer", or "aliasing XOR mutability". Aliasing means two names that can reach the same data. Rust allows aliasing, and it allows mutation, but never both at the same time on the same data.
| While this exists | Other &T allowed? | A &mut T allowed? | Owner can read? | Owner can modify or move? |
|---|---|---|---|---|
One or more &T | Yes | No | Yes | No |
One &mut T | No | No | No (use the reference) | No |
| No borrows | Yes | Yes | Yes | Yes |
Why "many readers or one writer"?
Shared mutation is behind a large family of bugs, and they are not only about threads:
- Iterator invalidation: changing a collection while iterating over it, as in modifying a Java
ArrayListinside a for-each loop (aConcurrentModificationExceptionat run time) or a C++std::vector(undefined behaviour). - Dangling interior pointers: holding a pointer into a vector or string while it reallocates, which you saw in lesson 1.
- Data races: two threads writing, or one writing and one reading, without synchronisation.
- Surprising aliasing: a function receives two parameters that turn out to be the same object and overwrites its own input.
Rust's rule makes all of these compile errors. As a bonus, the compiler knows that data behind a &T cannot change and that a &mut T has no other alias, which enables optimisations C compilers can only make when the programmer promises it with restrict.
Reading the compiler error: E0499, two mutable borrows
This does not compile:
fn main() {
let mut s = String::from("data");
let r1 = &mut s;
let r2 = &mut s;
r1.push('1');
r2.push('2');
println!("{s}");
}
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> two_mut_err.rs:4:14
|
3 | let r1 = &mut s;
| ------ first mutable borrow occurs here
4 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
5 | r1.push('1');
| -- first borrow later used here
The three labels are the standard borrow-checker story: where the first borrow starts, where the conflicting borrow happens, and where the first borrow is used later, proving it was still alive. That last label is the key. If r1 were never used after r2 was created, the program would compile, as the next section shows.
Reading the compiler error: E0502, reading and writing at once
This does not compile:
fn main() {
let mut scores = vec![70, 85, 90];
let first = &scores[0];
scores.push(60);
println!("first = {first}");
}
error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
--> mixed_err.rs:4:5
|
3 | let first = &scores[0];
| ------ immutable borrow occurs here
4 | scores.push(60);
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("first = {first}");
| ----- immutable borrow later used here
push needs &mut scores (it may reallocate the buffer), while first holds a & into that buffer and is used afterwards. If this compiled, first could point into freed memory after the push. This is the same bug you saw in lesson 1, and it is the most common borrow error in real code.
Non-lexical lifetimes: a borrow lasts until its last use
How long does a borrow last? Early versions of Rust said "until the end of the scope of the reference variable". That rejected a lot of correct code. Since the 2018 edition, Rust uses non-lexical lifetimes (NLL): a borrow lasts from where it is created until the last place the reference is used, not until the closing brace.
fn main() {
let mut scores = vec![70, 85, 90];
let first = &scores[0];
println!("first = {first}");
scores.push(60);
println!("scores = {scores:?}");
let r1 = &mut scores;
r1.push(50);
let r2 = &mut scores;
r2.push(40);
println!("scores = {scores:?}");
}
Output:
first = 70
scores = [70, 85, 90, 60]
scores = [70, 85, 90, 60, 50, 40]
Why this compiles, step by step:
line borrows alive afterwards
let first = &scores[0]; first (shared)
println!(... first ...); none: last use of first
scores.push(60); (temporary &mut during the call)
let r1 = &mut scores; r1
r1.push(50); none: last use of r1
let r2 = &mut scores; r2
r2.push(40); none: last use of r2
println!(... scores ...); (temporary & during the call)
first, r1 and r2 are all still "in scope" until the end of main, but their borrows end at their last use. No two conflicting borrows are ever alive at the same time.
How to fix most E0502 and E0499 errors
Look at the label "borrow later used here". Ask whether you can move that use earlier, before the conflicting change, or copy the value out (let first = scores[0]; copies an i32 instead of borrowing it). Very often, reordering two lines or copying a small value is the whole fix.
Reading the compiler error: changing a collection while looping over it
This does not compile:
fn main() {
let mut nums = vec![1, 2, 3];
for n in &nums {
if *n % 2 == 1 {
nums.push(*n * 10);
}
}
println!("{nums:?}");
}
error[E0502]: cannot borrow `nums` as mutable because it is also borrowed as immutable
--> iterate_push_err.rs:5:13
|
3 | for n in &nums {
| - -----
| | |
| | immutable borrow occurs here
| _____| immutable borrow later used here
| |
4 | | if *n % 2 == 1 {
5 | | nums.push(*n * 10);
| | ^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
6 | | }
7 | | }
| |_____- this for loop borrows `nums` immutably, preventing mutation within its body
|
= help: consider using an index-based loop instead, or collecting modifications into a separate collection
for n in &nums borrows nums for the entire loop, so pushing inside the loop is a conflicting mutable borrow. The help line names the two standard fixes. Collecting the changes first and applying them afterwards looks like this:
fn main() {
let mut nums = vec![1, 2, 3];
let extra: Vec<i32> = nums
.iter()
.filter(|n| *n % 2 == 1)
.map(|n| n * 10)
.collect();
nums.extend(extra);
println!("{nums:?}");
}
Output:
[1, 2, 3, 10, 30]
The first statement builds a new vector from the odd numbers multiplied by ten, while nums is only borrowed for reading. Once that borrow ends, extend borrows nums mutably to append the results.
Dangling references
A dangling reference points to memory that has been freed or reused. In C it is easy to create one by returning a pointer to a local variable. Rust's rule 2 makes it impossible.
Reading the compiler error: E0106, returning a reference to nothing
This does not compile:
fn make_label() -> &String {
let label = String::from("draft");
&label
}
fn main() {
let l = make_label();
println!("{l}");
}
error[E0106]: missing lifetime specifier
--> dangle_err.rs:1:20
|
1 | fn make_label() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
1 | fn make_label() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
1 - fn make_label() -> &String {
1 + fn make_label() -> String {
|
The error mentions a lifetime: the compiler's name for the region of code during which a reference is valid. A function returning a reference must say which input it borrows from, so that callers know how long the result is valid. make_label has no inputs, so there is nothing for the result to borrow from, and the compiler stops at the signature. label would be dropped at the end of the function, so a reference to it would dangle.
The help offers two fixes, and the second is almost always right: return an owned String. The first, 'static, is only correct for data that really lives forever, such as a string literal:
fn make_label() -> String {
String::from("draft")
}
fn default_label() -> &'static str {
"draft"
}
fn main() {
println!("{} {}", make_label(), default_label());
}
Output:
draft draft
If the function does have a reference input, the error changes. Here is a version that builds a new string from its input and tries to return a reference to it. This does not compile:
fn initials(name: &str) -> &str {
let mut out = String::new();
for part in name.split_whitespace() {
if let Some(c) = part.chars().next() {
out.push(c);
}
}
&out
}
fn main() {
println!("{}", initials("Sachin Ramesh Tendulkar"));
}
error[E0515]: cannot return reference to local variable `out`
--> ex_dangle_err.rs:8:5
|
8 | &out
| ^^^^ returns a reference to data owned by the current function
E0515 says it plainly: out is owned by the function and dies when it returns. Return String instead. You will see this program fixed in the exercises.
Reading the compiler error: E0597, borrowed value does not live long enough
The same rule applies inside a single function. This does not compile:
fn main() {
let r;
{
let value = String::from("short-lived");
r = &value;
}
println!("{r}");
}
error[E0597]: `value` does not live long enough
--> scope_err.rs:5:13
|
4 | let value = String::from("short-lived");
| ----- binding `value` declared here
5 | r = &value;
| ^^^^^^ borrowed value does not live long enough
6 | }
| - `value` dropped here while still borrowed
7 | println!("{r}");
| - borrow later used here
value is dropped at the end of the inner block, but r is used after that. The labels show the three facts the compiler used: where value lives, where it is dropped while still borrowed, and where the borrow is later used. Fix it by moving the println! inside the block, or by declaring value in the outer scope so it lives long enough.
outer scope |-- r ------------------------------------| used here
inner scope |-- value --|
^ r = &value
^ value dropped: r would dangle
Lesson 13 covers lifetimes in full, including writing lifetime annotations such as 'a. For now, remember the principle: a reference can never outlive its owner, and when it might, the fix is usually to return or store owned data instead.
Slices
A slice is a reference to a contiguous run of elements inside some collection, rather than to the whole collection. A slice is a fat pointer: a pointer to the first element plus a length.
String slices: &str
A &str is a slice of UTF-8 text. You create one from a String with a range:
fn main() {
let sentence = String::from("hello rust world");
let hello: &str = &sentence[0..5];
let world = &sentence[11..];
println!("[{hello}] [{world}]");
let marks = [55, 67, 72, 81, 94];
let middle: &[i32] = &marks[1..4];
println!("middle = {middle:?}, len = {}", middle.len());
let literal: &str = "I am a slice too";
println!("{literal}");
}
Output:
[hello] [world]
middle = [67, 72, 81], len = 3
I am a slice too
sentence: String
+-----+-----+-----+
| ptr | 16 | 16 |
+--+--+-----+-----+
|
v 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+--------------------------------------+
| h e l l o r u s t w o r l d | heap bytes
+--------------------------------------+
^ ^
hello: &str world: &str
+-----+-----+ +-----+-----+
| ptr | 5 | | ptr | 5 |
+-----+-----+ +-----+-----+
Range shorthand works as you would expect: &s[..5] starts at 0, &s[11..] runs to the end, and &s[..] is the whole string. String literals are slices too: "I am a slice too" has type &'static str, a slice pointing into the program binary.
The same idea applies to arrays and vectors. &marks[1..4] is a &[i32] covering elements 1, 2 and 3. The original array is untouched; the slice is just a view onto it.
Slice indices are byte offsets
For strings, the numbers in a range are byte positions, not character positions. With ASCII text they coincide. With other text they do not, and slicing in the middle of a character panics:
fn main() {
let city = String::from("Bengaluru");
println!("{}", &city[0..4]);
let word = String::from("नमस्ते");
let end: usize = "2".parse().unwrap();
println!("{}", &word[0..end]);
}
Output:
Beng
thread 'main' (1680648) panicked at char_boundary.rs:7:25:
end byte index 2 is not a char boundary; it is inside 'न' (bytes 0..3 of string)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
न takes three bytes in UTF-8, so byte 2 is in the middle of it. Rust refuses to create a &str that is not valid UTF-8, so it panics instead. (The number after 'main' is a thread identifier and differs between runs.) When working with non-ASCII text, use char_indices() to find valid boundaries, or chars() to work character by character. Lesson 8 goes deeper.
Slices keep the borrowing rules
A slice is a reference, so it borrows its collection. This is what makes slices safe. Here is a first_word function that returns a slice of its input:
fn first_word(s: &str) -> &str {
for (i, byte) in s.bytes().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
fn main() {
let owned = String::from("borrow checker");
println!("{}", first_word(&owned));
println!("{}", first_word("lifetimes"));
println!("{}", first_word(&owned[7..]));
}
Output:
borrow
lifetimes
checker
first_word scans the bytes for a space and returns the slice before it, or the whole input if there is no space. Because the parameter is &str, it works with a borrowed String, a string literal, and a slice of a String.
Now suppose a caller keeps the returned word and then clears the string. In a language without borrow checking, the word would now refer to cleared or freed text. In Rust this does not compile:
fn first_word(s: &str) -> &str {
s.split(' ').next().unwrap_or("")
}
fn main() {
let mut text = String::from("hello world");
let word = first_word(&text);
text.clear();
println!("first word: {word}");
}
error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable
--> first_word_err.rs:8:5
|
7 | let word = first_word(&text);
| ----- immutable borrow occurs here
8 | text.clear();
| ^^^^^^^^^^^^ mutable borrow occurs here
9 | println!("first word: {word}");
| ---- immutable borrow later used here
The compiler knows the returned &str borrows from text (lesson 13 explains how it knows), so word keeps text borrowed until its last use, and clear cannot get a mutable borrow. A whole category of "stale index" bugs, where you keep a position into data that has since changed, disappears.
Designing function signatures that borrow
Choosing parameter types is where ownership and borrowing become design. A simple decision table:
| The function needs to... | Parameter type | Example |
|---|---|---|
| Read text | &str | fn shout(text: &str) -> String |
| Read a sequence | &[T] | fn total(values: &[i32]) -> i32 |
| Modify a sequence's elements in place | &mut [T] | fn clamp_scores(scores: &mut [i32]) |
| Grow, shrink or replace a collection | &mut Vec<T> or &mut String | fn add_exclamation(text: &mut String) |
| Keep the value (store it, return it, send it to a thread) | Owned String or Vec<T> | fn set_name(&mut self, name: String) |
Read a small Copy value | The value itself | fn square(x: i32) -> i32 |
Prefer &str and &[T] over &String and &Vec<T>
This program compiles and runs:
fn total(values: &Vec<i32>) -> i32 {
values.iter().sum()
}
fn shout(text: &String) -> String {
text.to_uppercase()
}
fn main() {
let v = vec![1, 2, 3];
let s = String::from("hi");
println!("{} {}", total(&v), shout(&s));
}
but Clippy objects:
warning: writing `&Vec` instead of `&[_]` involves a new object where a slice will do
--> ptr_arg.rs:1:18
|
1 | fn total(values: &Vec<i32>) -> i32 {
| ^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#ptr_arg
= note: `#[warn(clippy::ptr_arg)]` on by default
help: change this to
|
1 - fn total(values: &Vec<i32>) -> i32 {
1 + fn total(values: &[i32]) -> i32 {
|
warning: writing `&String` instead of `&str` involves a new object where a slice will do
--> ptr_arg.rs:5:16
|
5 | fn shout(text: &String) -> String {
| ^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#ptr_arg
help: change this to
|
5 - fn shout(text: &String) -> String {
5 + fn shout(text: &str) -> String {
|
A &String parameter only accepts a borrowed String; callers with a string literal or a slice of a bigger string would have to allocate a new String first. A &str parameter accepts all of them, because &String coerces to &str automatically. The same goes for &Vec<i32> versus &[i32], where the slice version also accepts arrays and sub-slices:
fn total(values: &[i32]) -> i32 {
values.iter().sum()
}
fn shout(text: &str) -> String {
text.to_uppercase()
}
fn main() {
let v = vec![1, 2, 3];
let arr = [10, 20];
let s = String::from("hi");
println!("{} {} {}", total(&v), total(&arr), total(&v[1..]));
println!("{} {}", shout(&s), shout("literal"));
}
Output:
6 30 5
HI LITERAL
One total function now handles a vector, an array and part of a vector: 1 + 2 + 3 = 6, 10 + 20 = 30, and 2 + 3 = 5.
Interview tip
When asked to write a function in a Rust interview, choose the parameter types out loud: "this only reads the text, so it takes &str; it returns new data, so it returns an owned String." Interviewers look for exactly this reasoning, and it shows you understand ownership rather than having memorised syntax.
Two mutable borrows of different parts
The borrow checker reasons about whole values in many cases. If you need mutable access to two different parts of one slice at the same time, &mut data[0] and &mut data[3] together are rejected, because the checker does not prove that indices differ. The standard library provides safe tools for this:
fn main() {
let mut data = [1, 2, 3, 4, 5, 6];
let (left, right) = data.split_at_mut(3);
left[0] = 100;
right[0] = 400;
println!("{data:?}");
let mut a = 1;
let mut b = 2;
std::mem::swap(&mut a, &mut b);
println!("a = {a}, b = {b}");
}
Output:
[100, 2, 3, 400, 5, 6]
a = 2, b = 1
split_at_mut(3) returns two non-overlapping mutable slices, the first three elements and the rest, so you can change both. std::mem::swap takes two mutable references and exchanges the values. (Borrowing two different fields of a struct mutably at once is fine, because the checker does track fields separately.)
Borrowing compared with other languages
| C pointers | C++ references | Java/Python references | Rust references | |
|---|---|---|---|---|
| Can be null | Yes | No | Yes (null/None) | No |
| Can dangle | Yes | Yes | No (GC keeps objects alive) | No (checked at compile time) |
| Shared mutation allowed | Yes | Yes | Yes | No: many readers or one writer |
| Cost of the checks | None | None | GC at run time | None at run time |
| Reference keeps the object alive | No | No | Yes | No: the owner decides; the reference must not outlive it |
The last row is a common source of confusion for Java and Python programmers. In those languages, holding a reference keeps an object alive. In Rust, the owner decides when a value dies, and the compiler rejects any reference that would outlive it.
Idioms and pitfalls
Pitfall: holding a reference across a mutation
Taking let x = &v[0]; and then calling v.push(..) or v.clear() while x is still used is the number one borrow error. Copy small values out (let x = v[0];), clone if needed, or finish using the reference before changing the collection.
Pitfall: returning a reference to a local
If a function creates a value, it must return the value itself, not a reference to it. "Missing lifetime specifier" on a function with no reference parameters, or E0515, almost always means "return an owned type".
Pitfall: slicing strings by character count
&s[0..3] takes three bytes, not three characters. For non-ASCII text this can panic. Use s.chars().take(3).collect::<String>() or char_indices() when you mean characters.
Idiom: the mutable borrow is temporary
Calling add_exclamation(&mut msg) does not make msg permanently borrowed. The borrow ends when the function returns (more precisely, at the last use of the reference), so you can use msg freely afterwards. Keep mutable borrows short and the borrow checker rarely gets in your way.
Exercises
Exercise 1: longest word
Write fn longest_word(text: &str) -> &str that returns the longest whitespace-separated word, or the empty string if there are no words. The result must borrow from the input, with no allocation.
Solution
split_whitespace yields &str slices of the input, so keeping the best one and returning it borrows from text. The compiler infers that the result borrows from the only reference parameter.
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 line = String::from("rust makes systems programming approachable");
println!("{}", longest_word(&line));
println!("[{}]", longest_word(""));
}
approachable
[]
"approachable" has 12 letters, beating "programming" with 11.
Exercise 2: clamp in place
Write fn clamp_scores(scores: &mut [i32]) that changes every score below 0 to 0 and every score above 100 to 100, without returning anything.
Solution
iter_mut gives a &mut i32 for each element, and clamp limits a value to a range. (If you write the comparisons by hand with if, Clippy's manual_clamp lint suggests exactly this.)
fn clamp_scores(scores: &mut [i32]) {
for s in scores.iter_mut() {
*s = (*s).clamp(0, 100);
}
}
fn main() {
let mut scores = vec![45, -10, 120, 88];
clamp_scores(&mut scores);
println!("{scores:?}");
}
[45, 0, 100, 88]
Exercise 3: one function, many callers
Write fn min_max(values: &[i32]) -> (i32, i32) and call it with a Vec, an array and a sub-slice of the vector.
Solution
Taking &[i32] lets all three callers use the same function. The for &v in values pattern copies each i32 out of its reference.
fn min_max(values: &[i32]) -> (i32, i32) {
let mut lo = values[0];
let mut hi = values[0];
for &v in values {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
(lo, hi)
}
fn main() {
let v = vec![7, -2, 15, 4];
let arr = [3, 9];
println!(
"{:?} {:?} {:?}",
min_max(&v),
min_max(&arr),
min_max(&v[2..])
);
}
(-2, 15) (3, 9) (4, 15)
&v[2..] is [15, 4], so its minimum and maximum are 4 and 15. Note that values[0] panics on an empty slice; a production version would return Option<(i32, i32)>, which lesson 7 introduces.
Exercise 4: fix the dangling return
Fix the initials function from the E0515 example so that it compiles and prints the initials of "Sachin Ramesh Tendulkar".
Solution
The function creates a new string, so it must return it by value.
fn initials(name: &str) -> String {
let mut out = String::new();
for part in name.split_whitespace() {
if let Some(c) = part.chars().next() {
out.push(c);
}
}
out
}
fn main() {
println!("{}", initials("Sachin Ramesh Tendulkar"));
}
SRT
Exercise 5: why does this fail?
Explain, without compiling, why the first_word_err program fails, and give two different fixes.
Solution
word is a &str that borrows from text, and it is used in the println! after text.clear(). clear needs a mutable borrow while the shared borrow held by word is still alive, which breaks "many readers or one writer" (E0502). Fix one: print word before calling clear, so the shared borrow ends first. Fix two: make word an owned copy with first_word(&text).to_string(), so it no longer borrows text.
Interview questions
Q1. What are the borrowing rules in Rust?
At any time 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 for as long as it is used, so it cannot outlive the value it points to. The borrow checker enforces both rules at compile time. This is often summarised as "aliasing XOR mutability".
Q2. Why does Rust forbid having a mutable reference and shared references at the same time?
Because mutation through one path while another path reads can invalidate what the reader sees: a vector reallocation can leave a reference dangling, iteration can be corrupted, and with threads it would be a data race. Forbidding the combination turns these bugs into compile errors. It also lets the compiler optimise on the assumption that data behind &T does not change.
Q3. What are non-lexical lifetimes?
Non-lexical lifetimes (NLL), introduced with the 2018 edition, mean a borrow lasts until the last use of the reference rather than until the end of the block where the reference variable was declared. This lets code create a reference, use it, and then mutate the original later in the same scope. Many programs that older compilers rejected now compile.
Q4. How does Rust prevent dangling references?
The compiler tracks how long each reference may be used and how long the referenced value lives, and rejects any program where a reference could be used after its value is dropped (E0597), or where a function returns a reference to its own local data (E0515, E0106). Since this is checked statically, there is no runtime cost. The usual fix is to return or store owned data.
Q5. What is a slice?
A slice is a reference to a contiguous sequence of elements inside a collection, represented as a pointer plus a length (a fat pointer). &str is a slice of UTF-8 text and &[T] is a slice of elements of type T. Slices borrow their collection, so the collection cannot be changed while the slice is in use.
Q6. Why should a function take &str rather than &String?
&str accepts more callers: borrowed Strings (through deref coercion), string literals, and slices of larger strings, while &String accepts only borrowed Strings. It is also one less level of indirection. Clippy's ptr_arg lint flags &String and &Vec<T> parameters for this reason.
Q7. What is the difference between &mut T and a mut binding?
let mut x makes the variable binding mutable: you can assign to x and take &mut x. &mut T is a type, a reference with exclusive access through which the value can be changed. You can have an immutable binding holding a &mut T (let r = &mut v; then r.push(1)), and a mutable binding holding a &T that you can repoint to other data.
Q8. Why does slicing a String with byte indices sometimes panic?
String slice ranges are byte offsets, and a &str must always be valid UTF-8. If an index falls inside a multi-byte character, the slice would be invalid, so Rust panics with "is not a char boundary". Use char_indices, chars or is_char_boundary when dealing with non-ASCII text.
Q9. Can you modify a Vec while iterating over it?
Not through the same vector in a for loop over &vec or &mut vec, because the loop holds a borrow for its whole duration, and adding or removing elements needs a conflicting mutable borrow (E0502 or E0499). You can modify elements in place with iter_mut, collect changes into a separate collection and apply them after the loop, use retain to remove elements, or loop over indices when you really must.
Q10. How can you get two mutable references into the same slice?
Not by indexing twice, because the borrow checker does not prove the indices differ. Use split_at_mut (or split_first_mut, chunks_mut, and similar methods), which return non-overlapping mutable slices, or std::mem::swap and slice::swap for exchanging values. These are safe APIs built on carefully checked unsafe code inside the standard library.
Q11. Does holding a reference keep a value alive in Rust, as it does in Java?
No. In Java or Python, a reference keeps the object alive until the garbage collector sees no references. In Rust the owner alone decides when a value is dropped, and the compiler rejects any reference that would be used after that point. Shared ownership with runtime reference counting exists (Rc, Arc, lesson 15), but it is opt-in.
Key takeaways
&Tborrows a value for reading;&mut Tborrows it exclusively for changing. Borrowing never moves or frees the value.- Many readers or one writer: shared references and a mutable reference to the same data cannot be alive at the same time.
- A borrow lasts until the reference's last use (non-lexical lifetimes), so reordering code or copying a small value out often fixes E0502 and E0499.
- A reference can never outlive its owner; returning a reference to a local fails with E0106 or E0515, and the fix is to return owned data.
- Slices (
&str,&[T]) are fat pointers into part of a collection and keep the collection borrowed while used. - String slice indices are byte offsets; slicing inside a multi-byte character panics.
- Take
&strand&[T]parameters for reading,&mutfor in-place changes, and owned types only when the function must keep the value.
Next lesson
Continue with Structs and methods in Rust.

