We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ground Rules: Values, Types, Control Flow step 23 of 24
String vs &str, part 2: functions that take and return text
Capitalise the first character of every word and join them with single spaces.
pub fn title_case(words: &[String]) -> String
["hello", "world"] gives "Hello World". [] gives "". ["a"] gives
"A". Everything after the first character is left exactly as it was, so
["rust", "IS", "fun"] gives "Rust IS Fun".
The starter does not compile. Two things are wrong with it, and you will meet them one at a time.
The signature is the lesson
You read the memory model in the previous item. Here is the operational rule it produces, and it is worth memorising in this form:
Take
&str. ReturnString. StoreString.
Take &str (or &[String], as here) because a borrowed view accepts every
caller: someone with a String passes &s, someone with a literal passes it
directly, someone with a sub-range passes &s[2..7]. Return String because
the caller needs something that outlives your function, and you are the one who
built it.
Clippy enforces the first half mechanically: ptr_arg rejects a
&String parameter and tells you to take &str, exactly as it rejected
&Vec<T> in item 1.20. The starter takes &Vec<String> and will be told so.
There is no lint for the return half — that one is on you.
E0369: &str + &str does not compile
The starter tries this:
let capitalized = &word[0..1] + &word[1..];
error[E0369]: cannot add `&str` to `&str`
|
| &word[0..1] + &word[1..]
| ----------- ^ ---------- &str
| |
| &str
= note: string concatenation requires an owned `String` on the left
This confuses everyone exactly once, and the reason is the type of +:
impl Add<&str> for String {
fn add(self, other: &str) -> String
}
+ is implemented for String + &str, and for nothing else. The left operand
must be an owned String and it is taken by value — so the addition
consumes it, reusing its buffer to hold the result instead of allocating a new
one. That is a deliberate performance decision, and it makes the ownership
visible:
let a = String::from("foo");
let b = a + "bar"; // `a` is MOVED here
// println!("{a}"); // error[E0382]: borrow of moved value
Contrast:
let a = String::from("foo");
let b = format!("{a}bar"); // `a` is only READ; still usable afterwards
That contrast is a memorable, low-stakes first ownership lesson: + moves,
format! borrows. format! is also the clearer choice for anything with more
than two pieces. What format! is not good for is wrapping something that is
already a String — clippy’s useless_format will tell you so.
Capitalising a character properly
Do not byte-slice. &word[0..1] takes the first byte, which is a
char_boundary panic waiting for the first non-ASCII input — and there is a
test with "élan" in it. Take the first character instead:
let mut chars = word.chars();
match chars.next() {
Some(first) => …, // `first` is a char
None => …, // the word was empty
}
chars.next() consumes the first character and leaves the iterator positioned
after it, so chars.as_str() hands you the untouched remainder as a &str.
That is the clean idiom for “first character and the rest” and it is correct for
every input.
char::to_uppercase returns an iterator, not a char, and this is not
pedantry: some characters uppercase to more than one. 'ß' uppercases to
"SS", and there is a hidden test for it. A function that returned a single
char would have to be wrong somewhere, so the standard library refuses to
offer one.
One more default-on lint
needless_borrow catches the &&String that beginners produce by taking a
reference to something that was already a reference — usually inside a closure
or a for pattern. If it fires, delete an ampersand.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.