Rust / Basic / Lesson 4
Borrowing, slices, and strings
Use temporary access without transferring ownership, end conflicting borrows before mutation, and treat UTF-8 byte boundaries explicitly.
On this page
Lesson goals
- Distinguish shared and exclusive mutable borrowing.
- Use &str and slices as borrowed interfaces.
- Separate byte length from Unicode scalar and display counts.
A reference grants access, not a new owner
An &T reference provides shared access to a valid T for an appropriate lifetime. An &mut T provides exclusive mutable access under Rust borrowing rules. References do not own the referred allocation or extend an arbitrary owner lifetime. Several shared borrows can coexist, but an active exclusive borrow conflicts with other accesses to the same data. Interior-mutability types introduce controlled exceptions through their APIs, not permission to ignore aliasing. Start by writing which operation observes and which operation mutates, then choose reference forms to match those requirements.
Sequence access around the last use
The compiler often tracks a borrow until its last relevant use rather than automatically keeping it active until the closing brace. This permits a mutation phase followed by an observation phase without unnecessary nested blocks. In the first example append receives a short exclusive borrow and returns before a shared slice is created. The owner remains alive throughout printing. A mutable reference itself is not Copy; passing it may involve reborrowing, so avoid explaining all reference use as unrestricted duplication. The function signature makes the mutation visible to callers.
fn append(text: &mut String) { text.push_str(" lab"); }
fn main() {
let mut title = String::from("Rust");
append(&mut title);
let view: &str = &title;
println!("{view}");
}
Rust lab
append changes the owned String, then its borrow ends. The later &str borrows the resulting live text.
A slice carries extent with a borrow
A slice such as &[u32] views a contiguous sequence and its length without owning elements. &str is a borrowed UTF-8 string slice, while String is an owned growable UTF-8 string. Prefer &str for read-only text parameters so callers can provide literals, owned strings, or valid substrings. Indexing a slice requires a valid index and can panic if the bound is wrong; get returns Option for checked access. A slice cannot remain in use across a vector mutation that might invalidate its backing storage, even when spare capacity appears sufficient.
Text has several legitimate length measures
String and str length counts bytes. chars counts Unicode scalar values, which still differ from user-perceived grapheme clusters in texts with combining marks or joined sequences. Rust does not support integer indexing into a string as if each visible character occupied one byte. A byte range must lie on UTF-8 boundaries. The second example uses an Arabic word with three scalar values encoded in six bytes and obtains its first two-byte scalar through get. General display-width and grapheme processing require a deliberate Unicode policy beyond this standard-library demonstration.
fn main() {
let word = "ΩΩΨ±";
println!("{} {}", word.len(), word.chars().count());
if let Some(first) = word.get(0..2) { println!("{first}"); }
}
6 3
Ω
The UTF-8 encoding has six bytes and three scalars. The range 0..2 is a valid boundary pair for the first letter.
Change the design before fighting diagnostics
When a borrow conflict occurs, locate the last use of every reference and the proposed mutation. Often the correct repair is to finish reading, store a small owned result, then mutate. If two disjoint portions must be changed simultaneously, a safe API such as split_at_mut can establish their non-overlap. Adding clone can be appropriate for a required snapshot but should not replace understanding the conflict. Do not cast to raw pointers to bypass a beginner borrowing error. That transfers obligations into unsafe code instead of proving the original access pattern valid.
Common errors and fixes
E0499 reports overlapping mutable borrows.
- Why it happens
- Two exclusive accesses remain live together.
- How to fix it
- End the first use or split provably disjoint data safely.
- Prevent it next time
- Keep mutation phases short and explicit.
E0502 reports a read/write borrow conflict.
- Why it happens
- A shared reference is still used after mutation begins.
- How to fix it
- Finish observation before acquiring the mutable borrow.
- Prevent it next time
- Track last uses rather than adding arbitrary braces.
A substring operation rejects a byte range.
- Why it happens
- A boundary cuts through a UTF-8 encoding.
- How to fix it
- Use valid boundaries or iterate chars for scalar processing.
- Prevent it next time
- Specify the desired text unit in requirements.
Check your understanding
1. A function reads a name and returns its byte count while the caller retains the String. Write the signature and predict the result for "Mira".
Show worked solution
Use fn byte_count(name: &str) -> usize and return name.len(). Calling byte_count(&owned_name) borrows rather than moves its String. Mira has four ASCII bytes, so the result is four. The caller may later mutate the string after the borrow ends, provided no reference from that borrow remains live.
2. For the Arabic word in the example, explain get(0..1) and get(2..4), including how callers should handle the results.
Show worked solution
get(0..1) returns None because one is inside the first two-byte scalar. get(2..4) returns Some("Ω") because both endpoints are valid boundaries around the second scalar. Match the Option or use if let rather than unwrapping an uncertain range. Neither byte range defines a universal method for extracting a user-visible character in arbitrary Unicode text.
Key takeaways
- Borrow only for the access duration required.
- Treat &str and slices as lifetime-dependent views.
- Choose bytes, scalar values, or graphemes explicitly.