References Borrowing Slices shared mut — the borrow that is not a copy
&s1 looks like it might copy the String, or take it. It does neither. The owner stays. The reference must die before the value does.
<!-- hal:authoritative:yaml -->
&s1 looks like it might copy the String, or take it. It does neither. The owner stays. The reference must die before the value does.
§I — Frame
Duha session 05. First fire of week 2 on the TRPL spine. The chapter is still Understanding Ownership. Session 04 took What Is Ownership: one owner, drop, move, clone, Copy, and the function boundary. Those proofs stand. Do not reopen them. Do not walk the String header again. Do not recast let s2 = s1; as today's hinge.
Session 04 stopped at Listing 4-5. calculate_length took a String and handed back a tuple so the caller could keep the value and also keep a length. That is too much ceremony for "use this, then let me use it again." The book's next sentence names the feature that removes the ceremony: references.
Coin the name now: the borrow that is not a copy.
calculate_length(&s1) looks like it might duplicate s1, or move it. It does neither. The ampersand builds a reference. The reference refers to a value some other name owns. The callee reads through that reference. s1 stays valid after the call. No clone. No tuple to give ownership back.
A slice is the same move at a smaller grain: a reference to a contiguous sequence inside a collection the slice does not own.
Done-criteria from the syllabus: you can explain shared versus mutable borrow, and you can say what a slice borrows.
The book's error listings still compile a crate named ownership. Use that name if you type along.
§II — A reference refers without owning
Listing 4-5's itch is gone the moment the signature takes &String instead of String:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
The tuple in the variable declaration is gone. The tuple in the return is gone. You pass &s1. The parameter is &String, not String. Those ampersands are references. They let you refer to a value without taking ownership of it. Figure 4-6 in the book draws &String s pointing at String s1.
A reference is an address you can follow to data stored at that address. That data is owned by some other variable. For the life of the reference, the compiler keeps the value valid.
The opposite of referencing with & is dereferencing with *. Some uses wait for Chapter 8. The details wait for Chapter 15. Do not start those chapters in this folder.
&s1 creates a reference that refers to s1 and does not own it. Because the reference does not own the value, nothing is dropped when the reference stops being used. s1 is still live for the println!.
The parameter s has the same scope any function parameter has. At the closing brace of calculate_length, s goes out of scope. The String is not dropped. s never had ownership. When a function takes a reference instead of the value, you do not return the value to give ownership back. You never had it.
That is the borrow that is not a copy. The three stack fields of the String are not copied in as a new owner. The heap is not cloned. Ownership does not move. The callee holds a followable address the compiler will keep valid for the life of that address.
§III — Borrowing, immutable by default
The book names the act of creating a reference borrowing. If a person owns a thing, you can borrow it. When you are done, you give it back. You do not own it.
What happens if you try to modify what you borrowed? Listing 4-6:
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
It does not compile.
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it
| refers to cannot be borrowed as mutable
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
Variables are immutable by default. References are too. A &String will not accept push_str. The help line names the other kind of borrow: &mut String. Read that as a different permission, not as a default.
§IV — Mutable references, one at a time
Fix Listing 4-6 with a mutable reference:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Three edits. s is mut. The call is &mut s. The parameter is &mut String. The signature now says, in the type, that change will mutate what it borrows.
Mutable references have one large restriction. If you have a mutable reference to a value, you can have no other references to that value. Two mutable references at once:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
7 | println!("{r1}, {r2}");
| -- first borrow later used here
r1 must last until the println!. Between creating r1 and using it, the listing tries to create r2 against the same data. The compiler refuses.
Most languages let you mutate whenever you like. The restriction is why new readers stall here. The gain is compile-time refusal of a data race. A data race is a race condition with three behaviors at once:
- Two or more pointers access the same data at the same time.
- At least one of those pointers is writing.
- No mechanism synchronizes access.
Data races are undefined behavior. They are hard to find at runtime. Rust will not compile the program that has them.
Curly braces make a new scope. Sequential mutable references are legal. Simultaneous ones are not:
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 is gone; a new mutable reference is legal
let r2 = &mut s;
}
If the first mutable borrow has ended, the second may start.
§V — Shared borrows, then a mutable one
The same rule covers mixing. Two shared borrows of s are fine. A mutable borrow of s while those shared borrows still count is not:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
println!("{r1}, {r2}, and {r3}");
}
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s;
| -- immutable borrow occurs here
6 | let r3 = &mut s;
| ^^^^^^ mutable borrow occurs here
8 | println!("{r1}, {r2}, and {r3}");
| -- immutable borrow later used here
A reader holding &s does not expect the bytes to change under that read. Several immutable references may exist together, because none of them can write. One writer, or many readers. Not both at the same time.
A reference's scope starts where it is introduced and continues through the last time that reference is used. Last use can be earlier than the closing brace. This compiles:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}");
let r3 = &mut s;
println!("{r3}");
}
r1 and r2 are last used in the first println!. Their scopes end there. r3 begins after that. The scopes do not overlap. The compiler can see the shared borrows are finished before the mutable borrow starts.
Hold the rule as a pair. At any given time: one &mut, or any number of &. The "at any given time" is last-use, not the brace.
§VI — Dangling references, then two rules
In languages with pointers, it is easy to free a region and keep a pointer to it. The pointer still names an address. The address may now hold someone else's data. Rust will not let a reference outlive the value.
Try to return a reference to a local:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &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
The diagnostic names lifetimes. Lifetimes are Chapter 10. Disregard that part today. The help line is the chapter's point: the return type contains a borrowed value, and there is no value for it to be borrowed from.
s is created inside dangle. When dangle finishes, s is dropped. Returning &s would hand the caller a reference to a String that no longer exists. The compiler stops you before that program runs.
Return the String instead. Ownership moves out. Nothing is deallocated at the brace:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
Two rules recap the section:
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
A slice is a different kind of reference. Same two rules. Smaller grain.
§VII — The index that is not tied to the buffer
Slices let you reference a contiguous sequence of elements in a collection. A slice is a kind of reference, so it does not have ownership. That is the borrow again, aimed at a span rather than at the whole value.
The book poses a small problem. Write a function that takes a string of words separated by spaces and returns the first word. If there is no space, the whole string is one word. For this section the book assumes ASCII. UTF-8 handling waits for Chapter 8.
Without slices you do not have a type that means "part of this string." You can return an index. Listing 4-7:
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
The parameter is already a borrow: &String. You do not need ownership to walk the bytes. Idiomatic Rust takes ownership of an argument only when the function must keep the value.
as_bytes yields the bytes. iter walks them. enumerate wraps each element as a tuple of index and reference. Patterns destructure that tuple: i is the index, &item is the byte. Iterators are Chapter 13. Patterns are Chapter 6. Today you need the byte literal b' ' and a return of the index, or s.len() if no space appears.
The usize is only meaningful next to that particular &String. It is a separate value. Nothing ties it to the buffer. Listing 4-8:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // 5
s.clear(); // s is now ""
// word is still 5
}
This compiles. word is 5. s has been emptied. The 5 no longer names a first word in s. Use it later and you have a bug the compiler will not catch.
A second_word would return (usize, usize). Start and end, both computed from a state they are not tied to. Three unrelated names to keep in sync with one buffer.
§VIII — String slices
A string slice is a reference to a contiguous sequence of the elements of a String:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
hello is not a reference to the whole String. It is a reference to a portion, named by the range in square brackets. [starting_index..ending_index]: start is the first position in the slice, end is one past the last position. Internally the slice stores a starting pointer and a length (ending_index minus starting_index). world points at byte 6 of s and has length 5. Figure 4-7 draws that header sitting beside the String.
Range sugar:
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2]; // same
let slice = &s[3..s.len()];
let slice = &s[3..]; // same
let slice = &s[0..s.len()];
let slice = &s[..]; // whole string
String slice range indices must sit at valid UTF-8 character boundaries. A slice in the middle of a multibyte character exits with an error. ASCII in this section keeps that door closed. Do not forget the door.
Rewrite first_word to return a slice. The type is &str:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
The search is the same. The return is &s[0..i] or &s[..]. One value, tied to the underlying data: a pointer to the start of the span, and a length. second_word can return &str the same way.
The Listing 4-8 bug becomes a compile error:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear();
println!("the first word is: {word}");
}
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
18 | s.clear();
| ^^^^^^^^^ mutable borrow occurs here
20 | println!("the first word is: {word}");
| ---- immutable borrow later used here
clear needs a mutable borrow to truncate. word is a slice, still used in the println!, so the immutable borrow is still active. Rule 1 from §VI fires. The program that compiled with a bare index now fails at compile time. The slice did not add a runtime check. It made the first-word result a borrow, so the two-rules already in your hands refuse the wipe.
That is still the borrow that is not a copy. &s[0..5] does not copy five bytes into a new String. It does not take the String. It borrows a span. The owner remains s. The span is valid only while that borrow is.
§IX — Literals as slices, &str as the parameter, other slices
A string literal is stored in the binary. Its type is &str: a slice pointing at that place in the binary. That is why literals are immutable. &str is an immutable reference.
let s = "Hello, world!";
A more experienced signature for first_word takes &str rather than &String. Listing 4-9. The same function then accepts a slice of a String, a whole String by reference, a slice of a literal, and a literal with no extra brackets:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
let word = first_word(&my_string);
let my_string_literal = "hello world";
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
let word = first_word(my_string_literal);
}
&my_string working where &str is expected uses deref coercions. Those wait for Chapter 15. Today, take the signature: a function that wants text without owning it should take &str.
Other slices follow the same header. An array:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
slice has type &[i32]. Pointer plus length, same as &str. You will meet this shape on vectors in Chapter 8. Do not open Chapter 8 today.
§X — Two proofs
The syllabus names two picks. Run them from the book.
Proof 1. Shared versus mutable borrow.
&T is a shared borrow. Many may exist at once. None of them may write. &mut T is a mutable borrow. Only one may exist, and no shared borrow may exist beside it, for as long as it is used. Last use ends the borrow, which may be earlier than the brace. Sequential mutable borrows in separate scopes are legal. Simultaneous ones are E0499. A mutable borrow while a shared borrow still counts is E0502. A write through &T is E0596. If you cannot say, before cargo run, which of those four you will get, walk §III through §V again.
Proof 2. What a slice borrows.
A slice borrows a contiguous sequence from a collection it does not own. &s[0..5] borrows five bytes of s. The header is a pointer and a length. The owner remains s. A string literal is already &str, a slice into the binary. first_word returning usize hands back a number with no tie to the buffer; s.clear() leaves that number lying. first_word returning &str hands back a borrow, so clear while the slice is live is E0502. Prefer &str as the parameter when the function needs text and does not need to own it.
A dangling return (dangle() -> &String) is the same rule aimed at the whole value: the reference must not outlive what it borrows. E0106 today. Lifetimes in Chapter 10.
§XI — Closing
Ch.4's second half is how you use a value without taking it. &s1 is the borrow that is not a copy. &mut s is the write permission, one at a time. A slice is a borrow of a span. The two rules close the chapter: one writer or many readers; references must stay valid.
Name it when &s1 looks like a copy of Listing 4-2 and behaves like Figure 4-6: the borrow that is not a copy. Ask whether the callee needs to own the value. If it does not, pass a reference. If it needs a span, pass a slice.
Session 06 is TRPL Ch.5: structs. Field and method syntax. Do not start it in this folder. Do not write it today.
Examine well. The &String parameter is the first proof's ground. The E0499 and E0502 listings are the restriction. The E0106 on dangle is the validity rule. The &str return on first_word is what a slice borrows. The Listing 4-8 index that still compiles is why the slice exists.
Related
- Prior fire: Duha session 01, the file that is not a crate
- Prior fire: Duha session 02, the guess that is not a number
- Prior fire: Duha session 03, the statement that is not an expression
- Prior fire: Duha session 04, the copy that is not a copy
- Syllabus: Duha Rust syllabus, session 05
- Grounding tome: TRPL Ch.4 References and Borrowing (Klabnik / Nichols, stable)
- Grounding tome: TRPL Ch.4 The Slice Type (Klabnik / Nichols, stable)
- Next fire: session 06, TRPL Ch.5 structs (unwritten)
🛡️ ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-09-02 · Duha · session 05 · TRPL Ch.4 · the borrow that is not a copy