9.7String slices
In lesson 5.4, you were told to pronounce &str "string slice" and to take the name on faith. Four chapters later, the faith pays out: a &str is exactly last lesson's machinery applied to text. It's a slice whose elements are the bytes of a string: an address, a length, and no ownership. Every fact you know about it (it views, it can't grow, it's cheap, literals are one) falls out of that.
The carving syntax works on Strings precisely like it worked on arrays:
fn main() {
let s = String::from("hello world");
let hello = &s[..5];
let world = &s[6..];
println!("{hello} | {world}");
}hello | world
hello and world are &strs, windows onto byte ranges of text that s owns on the heap. No text was copied; two handles were made. And the indexes count bytes, not characters, a distinction lesson 5.3's "café" made you respect; we'll meet its consequences at the end of the lesson.
Literals, and the spelling from lesson 4.7
A string literal's text is baked into your compiled program (lesson 5.4), so let motto = "fearless"; makes motto a slice into the program binary itself: a view of text that's alive from the first instruction to the last. Rust has a name for that longest-possible lifetime, and you've seen it: lesson 4.7's quiz had you write &'static str on faith, and last lesson's E0106 transcript offered 'static as the compiler's first (unlikely) suggestion. The 'static is a lifetime name, the only one you'll see written out until chapter 17, and it means "lives for the entire program run." A view of the binary can promise that; a view of a local String can't, which is why the compiler called the suggestion uncommon.
The function that needs a slice
Here's the problem string slices were born for. Write a function that finds the first word in some text. Without slices, the best you can return is where the word ends:
fn first_word_end(s: &String) -> usize {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b' ' {
return i;
}
i += 1;
}
s.len()
}
Two small novelties, both honest previews. s.as_bytes() lends the String's raw bytes as a &[u8], last lesson's slice type with lesson 4.2's smallest integer. And b' ' is a byte literal: the space character as its byte value (a u8), so it can be compared against the slice's elements. The while-and-index scan is lesson 9.6's pattern. We walk the bytes; at the first space we return its position; no space means the whole string is one word, so we return its length.
It compiles, it returns 5 for "hello world", and it has a bug you can't see at the return site. The usize it returns is only about the String it scanned; nothing ties them together. Watch the answer outlive its question:
fn main() {
let mut s = String::from("hello world");
let end = first_word_end(&s);
s.clear();
println!("the first word ends at byte {end}");
}the first word ends at byte 5
clear is new but unsurprising: it empties the String (a writable-loan method, like push_str). The program compiles, runs, and confidently reports a fact about text that no longer exists. end says 5; s is empty; both are "right"; the combination is a lie. The C++ chapter this lesson adapts spends real estate teaching the discipline to manage exactly this kind of stale bookkeeping by hand.
The slice version
Return a view of the word instead of a number about it, and the bookkeeping becomes the borrow checker's job:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b' ' {
return &s[..i];
}
i += 1;
}
s
}
Same scan, but the answer is now &s[..i], a slice of the input up to the space (or the whole input, s, if it's all one word). The return type is &str, the signature is lesson 9.5's safe shape (a view borrowed from a parameter), and the answer is now physically attached to the text it describes. Run the same betrayal as before:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear();
println!("the first word is: {word}");
}error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:16:5
|
15 | let word = first_word(&s);
| -- immutable borrow occurs here
16 | s.clear();
| ^^^^^^^^^ mutable borrow occurs here
17 | println!("the first word is: {word}");
| ---- immutable borrow later used here
Patient 2 from lesson 9.3, recognized on sight by now. But pause on what just happened, because it's the chapter's thesis in one transcript. The index version had the same logical bug and compiled. The slice version makes the bug a type error, because the connection between answer and text, which used to live in a comment or the programmer's memory, now lives in the reference where the borrow checker can see it. Rust isn't just protecting memory here; the memory in the index version was never unsafe. It's protecting the meaning of your data. Reorder (print the word, then clear) and it compiles happily, printing the first word is: hello.
Why &str beats &String, mechanically at last
Notice the parameter: first_word(s: &str), not &String, even though main passes &s where s is a String. Lesson 5.4 told you this works and promised the mechanism in chapter 9. The mechanism is called deref coercion: where a &str is expected and a &String is supplied, the compiler automatically converts, by handing over a view of all the String's text. (Last lesson's array-to-slice conversion is a sibling rule with the same shape: owner-flavored reference in, view out.)
The arithmetic of the choice: a &str parameter accepts a &String (coerced), a literal (already a &str), or any slice of either (&s[..5]). A &String parameter accepts only the first. Same function body either way, strictly more callers served, which makes 5.4's best practice a closed case instead of a house rule.
Best practice
Take &str for read-only text parameters, always. The same logic now extends up a level: prefer &[i32] over &[i32; 5], and in general, take the widest view type that can do the job.
The byte-boundary fine print
One warning before the chapter closes, owed since "café". Slicing counts bytes, but a char can occupy up to four of them (lesson 4.8), and Rust refuses to manufacture a view of half a character. Cut at a clean boundary and all is well; cut mid-character and the program panics. With the cut point from input (the compile-time-arithmetic lesson of 3.1 and 4.4 applies to text too):
use std::io;
fn main() {
let order = String::from("café au lait");
let mut end = String::new();
io::stdin().read_line(&mut end).expect("failed to read line");
let end: usize = end.trim().parse().expect("not a number");
let prefix = &order[..end];
println!("prefix: {prefix}");
}
Type 3 and you get prefix: caf. Type 4, which lands between é's two bytes:
thread 'main' (2304854) panicked at src/main.rs:10:24:
end byte index 4 is not a char boundary; it is inside 'é' (bytes 3..5 of string)
The panic message is a small masterpiece: which index, why it's illegal, which character it stabbed, and that character's true byte range. For the English-y examples of the next few chapters, byte positions you found by scanning the text (like first_word's space positions) are always safe cut points; arbitrary numbers from elsewhere are not. The full toolkit for walking text character-by-character arrives with chapter 18's deeper String coverage.
Quiz time
Question #1
Name the type of each variable, no compiler allowed:
fn main() {
let motto = "fearless";
let owned = String::from("fearless concurrency");
let word = &owned[..8];
let counts = [3, 1, 4];
let pair = &counts[..2];
println!("{motto} {owned} {word} {pair:?}");
}Show solution
motto is a &str (a literal: a view into the binary, and the one variable here entitled to the full &'static str spelling). owned is a String. word is a &str, an 8-byte view into owned's heap text. counts is an array, [i32; 3]. pair is a &[i32], last lesson's slice. Two slice types, one concept, four total characters of difference in their spellings.
Question #2
Using this lesson's first_word, predict the compiler's reaction, then fix by reordering:
fn main() {
let mut headline = String::from("rust ships");
let lead = first_word(&headline);
headline.push_str(" today");
println!("{lead}");
}Show solution
E0502: lead is a live read-only borrow of headline (it's a view into it, used at the println!), and push_str needs a writable loan in between. And it's the full horror scenario from lesson 9.2: growing headline could relocate its text, leaving lead pointing at a freed block, so this refusal is load-bearing. Fix: print first, push after.
fn main() {
let mut headline = String::from("rust ships");
let lead = first_word(&headline);
println!("{lead}");
headline.push_str(" today");
}
This prints rust.
Question #3
Write fn last_word(s: &str) -> &str, returning everything after the final space (or the whole string if there's no space). Use first_word's scanning pattern.
Show solution
fn last_word(s: &str) -> &str {
let bytes = s.as_bytes();
let mut start = 0;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b' ' {
start = i + 1;
}
i += 1;
}
&s[start..]
}
Instead of returning at the first space, keep scanning and remember the position just past the latest space seen; the answer is the slice from there to the end. With no spaces, start stays 0 and the whole string comes back, matching first_word's behavior for one-word input. (last_word("hello world") is "world"; and the returned view borrows from s, so the borrow checker guards it exactly like first_word's.)
That's the chapter's full toolkit: two kinds of loans, two rules, four famous refusals, and views you can aim at any part of a sequence. The summary lesson collects it all in one place and then makes you use every piece at once.