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
Implement both functions. Each returns a slice of the input: a &str
that points into s rather than a newly allocated String.
The contract
The only separator is the ASCII space character ' ' (U+0020). Tabs,
newlines and other whitespace are ordinary characters here.
first_wordreturns everything before the first space. Ifscontains no space, it returns all ofs.last_wordreturns everything after the last space. Ifscontains no space, it returns all ofs.
input first_word last_word
"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"
" " "" ""
Rules that follow from this definition:
- An empty result is valid. A space at the start makes
first_wordempty, and a space at the end makeslast_wordempty. - Runs of spaces are not merged. In
"a b",first_wordis"a"andlast_wordis"b", because only the first and the last space matter. - Non-ASCII text is allowed anywhere and is returned unchanged. For example,
first_word("héllo wörld")is"héllo".
The grader calls both functions on the same input and compares the two returned strings exactly.
Why the starter fails
The starter uses split_whitespace(). That method splits on any Unicode
whitespace, treats a run of whitespace as one separator, and skips leading
and trailing whitespace. So for " lead" it returns "lead" as the first
word, and for "trail " it returns "trail" as the last word, while this
contract requires "" in both places. It would also split "a\tb" at the
tab. split_whitespace() is a useful method; it just computes a different
rule from the one tested here.
What a slice is
A string slice &str (and, for other element types, &[T]) is a borrowed
view of data that something else owns. At run time it is a pointer and a
length. Making one with &s[..i] does not allocate and does not copy the
text. For &str, the range is checked first (it must lie within the string
and fall on character boundaries, explained below), so slicing is a
constant-time operation, not a free one.
Because the slice is a borrow, its type records which data it borrows from.
The compiler then refuses any use of the slice after that data could have
been changed or freed. C++'s std::string_view is also a pointer and a
length, but the C++ language does not track what it points into. Some
compilers warn about simple cases, but in general a view that outlives its
string compiles, and using it is undefined behavior.
The following comparison is adapted from The Rust Programming Language (chapter 4.3).
Returning an index. Suppose first_word returned the byte index where
the first word ends:
fn first_word_end(s: &str) -> usize {
s.find(' ').unwrap_or(s.len())
}
fn main() {
let mut s = String::from("hello world");
let end = first_word_end(&s); // 5
s.clear(); // compiles: `end` is a plain number, not a borrow
// `end` is still 5, but `s` is now empty, so the index is stale.
// `&s[..end]` would panic here because 5 is past the end.
println!("{end}");
}
The number 5 only meant something for the string as it was when
first_word_end ran. Nothing in the type usize records that link, so the
compiler cannot warn you when it breaks.
Returning a slice. With the signature from this exercise, the same mistake does not compile:
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
let mut s = String::from("hello world");
let w = first_word(&s); // `w` borrows from `s`
s.clear(); // error[E0502]: cannot borrow `s` as mutable
// because it is also borrowed as immutable
println!("{w}");
}
clear needs a mutable borrow of s, and w is a shared borrow of s
that is still used on the next line. The check happens entirely at compile
time; the slice has no run-time bookkeeping. Putting the relationship in the
return type is what lets the compiler reject this particular kind of stale
reference.
Why no lifetime annotation is needed
The signature does not name a lifetime, but it means the same as:
pub fn first_word<'a>(s: &'a str) -> &'a str
This works because of the lifetime elision rules. A lifetime is the
compiler's name for the region of code in which a borrow is valid. When a
function signature leaves lifetimes out, each elided lifetime in the
parameters gets its own lifetime parameter, and if the parameters use exactly
one lifetime, that lifetime is given to every elided lifetime in the return
type. (A further rule covers methods that take &self or &mut self.) Here
there is one reference parameter, so the returned &str borrows from s.
With two reference parameters the rules no longer pick one, so this does not compile:
fn longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
// error[E0106]: missing lifetime specifier
You fix that by writing the lifetime parameters yourself, which is outside the scope of this exercise.
The elided signature also limits what the function can return. It may return
a borrow of s, or data that lives for the whole program, such as the
string literal "" (type &'static str). It cannot return a slice of a
String created inside the function, because that String is dropped when
the function returns:
fn first_word_upper(s: &str) -> &str {
let upper = s.to_uppercase();
&upper[..]
}
// error[E0515]: cannot return value referencing local variable `upper`
Slicing a &str uses byte offsets
The positions in &s[..i] are byte offsets into the UTF-8 encoding, not
character counts. Slicing panics if an end point is past the end of the
string or falls inside a multi-byte character. find(' ') and rfind(' ')
return the byte offset where a space starts, and a space is one byte long, so
both that offset and the offset just after it (i + 1) are always valid
slice points. A number computed some other way, such as by counting
characters, may not be.
Your submission is graded on three things: it must compile, pass the test
cases, and pass cargo clippy -- -D warnings (every Clippy warning is
treated as an error). One lint you may meet: writing &s[..] when s is
already a &str produces an identical slice and is reported by
clippy::redundant_slicing. Clippy also has an opt-in lint,
clippy::string_slice, for projects that want to forbid &str range
slicing entirely; it is not enabled by default and is not part of this
grade.
The diagram below shows the same idea for a Vec: two slices borrow
different parts of v at once, and both are shared borrows of v.
Loading visualization…
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.