Strings, For Real This Time 🧵
Every language makes strings look simple. Rust makes them look exactly as complicated as they really are, which feels rude until the day a user named José shows up and your program does not crash.
The two strings, one more time 👯
You have been using both since Level 2. Now let's name the difference
precisely. &str is a view into text that
lives somewhere else: fixed size, borrowed, cheap. String is
owned, growable text on the heap, like a Vec
of characters you can push onto:
fn main() {
let s: &str = "hello"; // borrowed view, fixed size
let mut owned: String = s.to_string(); // owned, growable
owned.push_str(", world");
println!("{owned}");
}
The rule of thumb you will use forever: take &str as
a function parameter, return String when you build new text.
A &String automatically shrinks into a &str when
passed, so functions that accept &str accept everything.
UTF-8: bytes are not letters 🌍
Here is the part most languages hide. Text is stored as UTF-8
bytes, and one on-screen character can take 1, 2, 3, or 4 bytes. An
e is one byte; an è is two; the crab 🦀 is four:
fn main() {
let word = "crème brûlée 🦀";
println!("chars: {}", word.chars().count());
println!("bytes: {}", word.len());
for c in "héy".chars() {
print!("[{c}]");
}
println!();
}
chars: 14
bytes: 20
Notice .len() counts bytes, not letters. Fourteen visible
characters, twenty bytes. Languages that pretend these are the same number
work great right up until they meet the real world.
Why you cannot index a string 🚫
In many languages word[0] gives you the first letter. Rust
refuses to compile it, and now you know why: word[0] would be
the first byte, which might be only a third of a letter. Rust will
not hand you a third of a letter. You can slice by byte ranges,
but only on character boundaries:
fn main() {
let word = "crème";
let first = &word[0..1]; // fine: 'c' is exactly 1 byte
println!("first: {first}");
let accent = &word[2..4]; // fine: 'è' occupies bytes 2 and 3
println!("accent: {accent}");
// let broken = &word[0..3]; // ❌ panics: byte 3 is the middle of 'è'
}
Uncomment that last line and run it: the panic message even tells you which
character you sliced through. When you actually want "the first letter,"
say what you mean: word.chars().next() gives an
Option<char>, honest as always.
char is not the full story. Some emoji, like family emoji
or flags, are several chars glued together into one visible
symbol, called a grapheme cluster. When that matters (cursors,
text editors), the unicode-segmentation crate handles it.
For most programs, chars() is exactly right.
The methods you will actually use 🛠️
fn main() {
let raw = " 42, rust, crab ";
let clean = raw.trim();
for part in clean.split(", ") {
println!("part: {part}");
}
let n: i32 = "42".parse().expect("not a number");
println!("doubled: {}", n * 2);
println!("{}", "rust is safe".replace("safe", "fast AND safe"));
println!("{}", clean.contains("crab"));
}
The greatest hits: trim, split and
split_whitespace, parse, replace,
contains, starts_with, to_uppercase.
Almost all of them return new values and leave the original alone,
which by now should feel very Rust.
Building strings 🧱
fn main() {
let words = vec!["fearless", "concurrency"];
let joined = words.join(" ");
println!("{joined}");
let mut banner = String::new();
banner.push_str("🦀 ");
banner.push_str(&joined);
banner.push('!');
println!("{banner}");
let shouted: String = joined.to_uppercase();
println!("{shouted}");
}
push_str appends a string slice, push appends a
single char, join glues a list together, and
format! (which you have used since Lesson 1) builds a
String from anything printable. That is 95% of real string work.
The word flipper
Take the sentence "rust makes strings honest" and print it
with every word reversed but the word order kept:
tsur sekam sgnirts tsenoh. You will want
split_whitespace, chars().rev(), and two
collects.
Reveal solution
fn main() {
let sentence = "rust makes strings honest";
let reversed: Vec<String> = sentence
.split_whitespace()
.map(|word| word.chars().rev().collect())
.collect();
println!("{}", reversed.join(" "));
}
Two collects doing two jobs: the inner one gathers reversed
chars into a String, the outer one gathers the
words into a Vec. And because you used chars()
instead of bytes, it even survives "héllo wörld". 🎉