Skip to content

← Ownership III: Lifetimes, Explicitly step 3 of 22

Easy Primitives

Borrow it or own it: your first lifetime decision

Implement two functions that return text in different ways:

pub fn first_word(s: &str) -> &str
pub fn shout(s: &str) -> String

first_word returns a slice of the input before its first ASCII space (' '). If there is no space, return the whole input. Do not trim the input: a leading space gives an empty result. Tabs, newlines and other Unicode whitespace are ordinary characters for this function.

shout returns an owned string containing the Unicode uppercase conversion of the entire input, followed by one additional !. Keep whitespace and existing punctuation. Use the conversion provided by str::to_uppercase.

Input first_word shout
"hello world" "hello" "HELLO WORLD!"
"" "" "!"
" lead" "" " LEAD!"
"trailing " "trailing" "TRAILING !"
"straße!" "straße!" "STRASSE!!"

Borrow a view, own a transformed result

For first_word, the desired bytes already form a prefix of s. Return that prefix without making a separate string. The result borrows the input and cannot be used after the borrowed data becomes invalid.

For shout, the return type is String, so the caller owns the result independently of the input. to_uppercase() returns a String; append the exclamation mark to it. Unicode conversion can expand one character into several, as with ß becoming SS. Do not assume one output character per input character or restrict the conversion to ASCII.

Why the starter fails

The starter copies s into a local String named owned, then tries to return a slice of that local string. Rust rejects this with E0515. owned would be dropped when the function returned, leaving the result pointing into storage that no longer belongs to a live string.

Return a slice of s instead. Lifetime annotations cannot extend the life of the local allocation. The signature already uses lifetime elision: with one input reference lifetime, the output reference receives that lifetime. The relationship is equivalent to fn first_word<'a>(s: &'a str) -> &'a str. You do not need to write the annotation to express it. See the lifetime-elision rules.

Keep the signature checks

The starter includes these compile-time checks:

const _: for<'a> fn(&'a str) -> &'a str = first_word;
const _: fn(&str) -> String = shout;

Each assignment checks that the function can be used with the stated function-pointer type. Returning String from first_word would fail the first check. Keep these declarations and the required function signatures. The grader also checks these function-pointer types independently of the starter.

These checks describe compatible types and lifetimes. They do not establish which allocation a returned reference points into, or whether a function allocates internally. The exercise still requires first_word to return a slice of its input.

Find a safe slice boundary

s.find(' ') returns an optional byte index. If a space is found, its start is a UTF-8 character boundary, so &s[..index] is a valid slice. If no space is found, use s itself. Counting characters and using that count directly as a byte index would not work for arbitrary UTF-8 input.

Splitting on the literal space is another valid approach. In particular, s.split(' ').next() has a first item even when s is empty, so an unwrap on that specific result does not introduce an empty-input panic. That guarantee does not extend to every iterator. split_whitespace() has different behavior and would change this task's leading-space and delimiter rules. The str documentation describes these methods.