Rust References, Borrowing and Slices¶
Borrowing lets a Rust function access a value without becoming its owner. This lesson builds on ownership to inspect and change a label, then view part of an array through a slice without copying the elements.
Prerequisites and outcome¶
Complete ownership and moves. You will distinguish &T from &mut T, explain when a borrow ends, and use &str and &[i32] as borrowed interfaces. All input is fixture data.
Run the borrowing example¶
Replace src/main.rs with:
Expected program output:
Rust shared and mutable references¶
&label creates a shared reference; it does not transfer ownership of the String. A shared reference does not permit ordinary mutation of that String. Multiple shared references can coexist.
&mut label gives exclusive mutable access for the borrow's duration. The owning binding must permit mutation. While that exclusive borrow is active, conflicting shared or mutable access is rejected. This is not a blanket claim that shared references prohibit all internal changes: interior-mutability types have their own contracts, covered later.
In our example, shared is last used before add_suffix. The compiler can end that borrow before the variable's enclosing block ends. If you use shared again after the mutation, the overlap becomes relevant and the compiler rejects the program. References must also remain within the validity of the data they refer to.
See references and borrowing. Do not think of mut as an instruction to disable borrow checking.
Binding mutability is not reference mutability¶
let mut reference = &label makes the reference binding reassignable; it still holds a shared reference and cannot ordinarily change the String. By contrast, let reference = &mut label grants mutable access even without mut on that reference binding. The former changes which reference the name holds; the latter can change the referred-to data.
For a numeric value, let reference = &mut reading; *reference = 61_000; writes through the reference. The * dereferences it. Method calls often perform the needed dereferencing automatically; label.push_str(...) inside add_suffix does not need an explicit *.
The compiler reasons about overlapping access, not simply the number of &mut tokens in a program. Sequential mutable borrows can be valid; disjoint slices can also be created through safe APIs such as split_at_mut. We do not bypass the checker with raw pointers here.
Slices are views, not owned copies¶
&readings[1..] is a borrowed slice over the final two elements. &[i32] lets the function accept different slice lengths, unlike [i32; 3]. Creating the view does not clone the array. A slice cannot outlive its backing data; invalid runtime ranges can panic.
Ranges use an exclusive upper bound: &readings[0..2] views the first two elements, &readings[..] views them all, and &readings[..0] is empty. A mutable slice such as &mut readings[1..] changes the original data, as the test shows; it is not a private copy. The slice API documents indexing and safe splitting.
&str is a borrowed view of valid UTF-8 text. The call to label_bytes(shared) uses dereference coercion from a reference to String to the text view it needs. That interface also accepts a literal, as the test demonstrates. len() counts bytes, not user-visible characters. Indexing text by arbitrary byte boundaries is not a character-access strategy.
The slice chapter explains this model. The String API documentation describes text operations and byte boundaries. String and Unicode operations receive their own lesson later.
The expected-panic test demonstrates why &"π"[..1] is invalid: byte 1 is inside the scalar's UTF-8 encoding. This is not a borrow-check error; it compiles and panics at runtime. The str API documents valid text boundaries. When data is external, checked access and meaningful errors are preferable to a deliberate panic.
Deliberately failing overlapping borrows¶
In a separate scratch project, use this complete program:
Run cargo check: expect a conflicting-borrow error (E0502). Move the print before the mutation to finish using the shared reference first. Cloning is not needed merely to reorder these operations.
Exercises and boundary cases¶
- Use
sharedagain afteradd_suffixin the working program: predict the conflict, check it, then restore the program. - Pass
&readings[..0]tofirst_reading: expect zero. This fallback intentionally loses the distinction between no reading and a real zero; lesson 9 replaces such conventions with Option. - Change the label to
π: expect two bytes before the suffix even though there is one Unicode scalar value. - Try returning a reference to a String created inside a function: expect a compile error. Returning owned data is one solution; lifetime annotations cannot keep a destroyed local allocation alive.
If a mutation fails to compile, locate the last use of every conflicting reference rather than adding mut everywhere. If a slice panics, validate its range; borrow checking does not prove every index is in bounds.
Verification and next step¶
On October 10, 2026, this lesson was verified on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Check, all 6 tests, formatting and debug/release output comparisons passed. The overlap example, later-reference-use exercise and attempted return of local borrowed data failed as expected. Empty-slice and Unicode-label variants also ran with the expected output. No live sensor or performance verification is claimed.
Continue with structs, methods and associated functions to group related values into a meaningful type.