We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership II: Borrowing and the Borrow Checker step 9 of 24
Slices are borrows
pub fn first_word(s: &str) -> &str
pub fn last_word(s: &str) -> &str
Both return a slice of the input, not a new string.
The semantics are pinned to the ASCII space character ' ', exactly:
-
first_wordreturns everything before the first space. If there is no space, the whole input. -
last_wordreturns everything after the last space. If there is no space, the whole input.
"hello world" -> "hello", "world"
"" -> "", ""
" lead" -> "", "lead" <- the first space is at index 0
"trail " -> "trail", "" <- the last space is the final byte
"single" -> "single","single"
"a b c" -> "a", "c"
Read the third and fourth lines carefully. The starter uses
split_whitespace(), which skips leading and trailing whitespace and
therefore gets both of those cases wrong. That is deliberate: half of all
submissions to a problem like this fail on ambiguous whitespace rules rather
than on the actual subject, so the rule here is stated and tested rather than
implied. split_whitespace() is a fine function; it just does not compute
what this problem asks for.
The payoff for the entire borrow system
This is the item where the machinery starts paying rent.
A slice — &str, &[T] — is a borrowed view of memory somebody else
owns: a pointer and a length, no allocation, no copying. &s[..i] is O(1) and
touches no bytes. It is exactly the thing C++ calls string_view and Go calls
a subslice.
The difference is that in C++ and Go, a view can outlive what it views, and
the result is a use-after-free or a silently mutated window. Every C++
codebase over a certain age has a string_view dangling bug. In Rust the
lifetime is part of the type, and the compiler will not let the view escape
the data.
Walk the Book’s progression to see why this is not a small thing.
Version 1 — return an index.
fn first_word(s: &str) -> usize { /* index of the first space */ }
let mut s = String::from("hello world");
let i = first_word(&s);
s.clear(); // fine! `i` is just a number
// ... and now `i` points into a string that no longer exists.
The usize has silently desanchored. It was meaningful with respect to a
particular string at a particular moment, and nothing in its type records
that. The bug is not detectable by the compiler, because there is nothing to
detect: usize is usize.
Version 2 — return a slice.
fn first_word(s: &str) -> &str { /* ... */ }
let mut s = String::from("hello world");
let w = first_word(&s);
s.clear(); // error[E0502]
println!("{w}");
The identical mistake is now a compile error, because w is a borrow of s
and clear needs exclusive access. Nothing was added to the runtime. The
return type simply carries information the usize threw away.
That is the trade the whole borrow system is making: put the relationship in the type, and a class of bugs becomes unrepresentable.
No lifetime annotation needed — and why
Notice what you did not write:
pub fn first_word<'a>(s: &'a str) -> &'a str
The elided form compiles and means exactly that. Rust’s lifetime elision rules say: each elided input lifetime gets its own parameter, and if there is exactly one lifetime-carrying input parameter, the elided output lifetime is taken from it. One input, one output, no ambiguity.
Take that same shape and add a second reference parameter and the elision stops working — you get E0106: missing lifetime specifier, because rustc will not guess which input the output borrows from. Track 8 is entirely about that. For now, appreciate that the common case needs no annotation at all.
The other code worth recognising is E0716: temporary value dropped while
borrowed — what you get if you try to return a slice of something you built
inside the function. You cannot; the local dies at the }. A &str return
can only ever be a view of an input.
Two practical warnings
Slicing is byte-indexed. &s[..i] counts bytes, and it panics if i
is not a UTF-8 character boundary. find(' ') and rfind(' ') return byte
offsets of a one-byte ASCII character, so the indices they hand you are always
safe. Slicing at an arbitrary number is not. (Clippy has a restriction lint,
string_slice, that bans &s[..] outright in codebases that want that
guarantee; it is allow-by-default and not part of this gate.)
Reslicing. &s[..] where s is already a &str is a no-op that clippy
flags as redundant_slicing. And a for i in 0..s.len() loop over a string is
both wrong (bytes, not chars) and lint-flagged (needless_range_loop).
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.