R Rust by Evidence PDF

Chapter 5 — String and &str

Goal

Choose between owned, growable string data and a borrowed view of UTF-8 text.

Prediction

fn announce(text: &str) {
    println!("{text}");
}

fn main() {
    let message = String::from("ready");
    announce(message);
}

What happens?

A. It prints because String and &str are identical types.
B. Compilation fails; the function expects &str but receives String.
C. Compilation fails because string literals are required.
D. It prints and consumes message.

Reveal: read the evidence

Answer: B.

error[E0308]: mismatched types
  |
7 |     announce(message);
  |     -------- ^^^^^^^ expected `&str`, found `String`
  |     |
  |     arguments to this function are incorrect
help: consider borrowing here
  |
7 |     announce(&message);
  |              +

The compiler does not invent a borrow at this call site. Add & to pass temporary shared access.

Build the model

String is an owned, growable UTF-8 string. It manages heap storage and can change when its binding and borrow state permit mutation:

let mut owned = String::from("red");
owned.push_str(" fox");

&str, pronounced “string slice,” is a borrowed view into valid UTF-8 bytes owned elsewhere. A string literal has type &'static str because its bytes are embedded in the program:

let literal: &str = "red fox";

A slice may also view part or all of a String:

let owned = String::from("red fox");
let all: &str = &owned;
let first: &str = &owned[..3];
println!("{first}, {all}");

Here deref coercion lets &String become &str where needed. The slice does not own the bytes, so it cannot outlive owned.

Working model: use String when code must own or build text. Use &str when code only needs to read text owned somewhere else.

That makes &str a strong default for read-only function parameters:

fn initials(name: &str) -> Option<char> {
    name.chars().next()
}

Both callers work without allocation:

fn initials(name: &str) -> Option<char> {
    name.chars().next()
}

let owned = String::from("Grace");
assert_eq!(initials(&owned), Some('G'));
assert_eq!(initials("Linus"), Some('L'));

A parameter of &String is less flexible: it asks specifically for a borrowed String, even though the function may only need text. Prefer &str unless String-specific capacity or mutation behavior is part of the contract.

String slicing uses byte indices, not character positions. This is valid because the boundaries surround complete UTF-8 characters:

let word = "café";
let cafe = &word[..5];
assert_eq!(cafe, "café");

But an index that cuts through the two-byte é will panic at runtime. For text processing by Unicode scalar values, use .chars(); for raw bytes, use .bytes(). Neither is the same as user-perceived grapheme clusters, which the standard library does not segment for you.

Precise model: str is a dynamically sized sequence of valid UTF-8 bytes. It is usually handled behind a pointer such as &str, which carries a data pointer and a length. String owns a UTF-8 buffer and can yield a &str view. &str is not limited to literals and does not necessarily cover an entire allocation.

Simplest fix

Borrow the owned string:

fn announce(text: &str) {
    println!("{text}");
}

fn main() {
    let message = String::from("ready");
    announce(&message);
    println!("still owned here: {message}");
}

No character data is copied.

Tradeoff alternative: accept ownership

fn queue(text: String) {
    // The queue could store `text` after this call.
    println!("queued: {text}");
}

fn main() {
    let message = String::from("ready");
    queue(message);
}

Accept String when the function needs to retain, mutate independently, or transfer the text. The tradeoff is that callers with &str must create an owned value, usually with .to_owned() or String::from, which allocates. Ownership should reflect a real need, not be the default for convenience.

Mini challenge

Which signature accepts both a string literal and a borrowed String without allocating or taking ownership?

A. fn show(text: String)
B. fn show(text: &String)
C. fn show(text: &str)
D. fn show(text: str)

Answer

C. Call it as show("literal") or show(&owned). String would take ownership and require allocation for the literal. &String does not directly accept a literal. Bare str is dynamically sized and cannot be passed by value as an ordinary parameter.

You now have the core questions for reading early Rust code: Which binding may change? Who owns each value? Did this operation move, copy, or borrow it? If borrowed, is access shared or exclusive, and how long is the reference actually used? For text, does this code need ownership, or only a &str view? Keep asking those questions; many intimidating compiler errors become precise answers.