Skip to content

← Ownership II: Borrowing and the Borrow Checker step 1 of 24

Easy Primitives

Move or borrow: the two ways to pass a value

Write two functions that both look at a string without taking it away from the caller.

pub fn char_count(text: &str) -> usize
pub fn shout(text: &str) -> String

char_count returns how many characters the text contains. shout returns a new String: the text uppercased, with a single ! appended.

""      -> 0,  "!"
"hi"    -> 2,  "HI!"
"héllo" -> 5,  "HÉLLO!"

The real subject: that &

In Track 2 you learned that passing a value moves it — the caller hands it over and can never touch it again. That is a strong guarantee, and it would be intolerable if it were the only option. You cannot write a function called char_count that destroys its argument.

So Rust has a second way to pass a value: you lend it. &T is a reference — a borrowed view of a value someone else owns.

If you come from C or C++, resist the word “pointer”. A Rust reference is narrower and stronger than a pointer:

  • it is never null. There is no if (p == NULL) in Rust. Absence is Option<&T>, a different type, and the compiler makes you handle it.
  • it is always valid. The compiler will not let a reference outlive the thing it points at. A dangling & is not a bug you can write.
  • it is temporary by construction. Every reference has a region of the program over which it is live, and the compiler tracks it. This is the whole subject of this track.
  • it is not a copy. &text does not duplicate the string’s bytes; it hands over a pointer and a length. Cheap, always.

Why &str and not &String

The signature is pinned to &str, and you should not “improve” it to &String. &String says “I need a reference to a heap-allocated, growable string specifically”. &str says “I need to look at some UTF-8 text, and I do not care where it lives”. The second is strictly more useful: it accepts a String, a string literal, a slice of a bigger string, all with no work at the call site.

Clippy enforces this with the ptr_arg lint, and this problem is graded with clippy -D warnings, so &String will fail the gate. Item 3.10 comes back to this and explains the machinery (deref coercion) that makes it free.

Characters are not bytes

text.len() gives the length in bytes, because that is the number a &str actually stores. "héllo" is five characters and six bytes: é is two bytes in UTF-8. The test suite contains that exact case, so .len() will not pass.

Counting characters means walking the text: .chars() yields one char per Unicode scalar value. That is O(n), which is why len() is not it.

(Even chars().count() is not “how many things a human sees” — a family emoji or a combining accent can be several chars. Track 5 goes into that. For this problem, chars are what is asked for.)

The proof that nothing was consumed

The test harness calls char_count(&text) and then shout(&text) on the same String. If either function took String by value, the second call would be E0382: borrow of moved value. The fact that the harness compiles at all is the proof that both of your functions only borrowed.

While you are writing shout, notice that you cannot move the text out of the reference either — let owned = *text; is E0507: cannot move out of a shared reference. A borrow lets you look, not take. You have to build a new String, which is exactly what to_uppercase does.

One warning about uppercasing: it is not a per-byte operation and it is not even length-preserving. German ß uppercases to SS — one character becomes two. The tests here stay clear of that case, but do not carry away the idea that uppercase is a cheap in-place flip. It is a table lookup over Unicode.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…