Skip to content

Strings

One line: Text in Rust is one pattern met over and over — an owner and a view — and almost every surprise in this section comes from the same place: the owner keeps its bytes on the heap, and those bytes are UTF-8, so an index into them is a byte offset rather than a character.

Two types do nearly all the work. String owns its text and can grow it; &str is a borrowed window onto text somebody else owns — including the binary itself, which is where a literal lives. A function takes &str because it only needs to read; a struct field owns a String because it has to outlive the call that built it.

The rest of the section is what follows from the bytes underneath. len() counts bytes, not characters. s[0] does not compile. + insists on an owned value on its left. And a slice whose endpoint lands inside a character panics at run time rather than at compile time — the one place strings ask you to be careful rather than letting the compiler be careful for you.

Lesson Level What it teaches
String vs &str 101 → 201 The owner and the view — a literal lives in the binary, not the stack; &String coerces to &str for free; and why parameters take &str while fields own String
String slices 101 → 201 A view is a pointer and a length — the stale byte index it replaces, the E0502 that keeps it honest, &s[..5], and the one way a slice panics: an index inside a character
The anatomy of a String 101 → 201 Three words on the stack, bytes on the heap — len is what you have, capacity is what you paid for, growth doubles, and the borrow checker's rule against a view held across a push_str
Making a String 101 → 201 Five spellings for one conversion — and why you implement Display, never ToString, plus the .to_string() on a String that is a silent clone
Concatenating strings 101 → 201 format!, + and join — the single Add impl behind all of it, and the three error codes you get from putting the wrong side on the left
Building a String 101 → 201 push_str, push, and the + that eats its left operand — format! vs write! in a loop, and why truncate panics where a slice does
Meet the char 101 → 201 One Unicode scalar, four bytes as a value, 1–4 inside a String — why .len() is not "how many characters", s[0] refuses to compile, and 'ß'.to_uppercase() returns two letters
Why a char is 32 bits wide 201 → 301 The arithmetic the primitive-types table leaves out — U+10FFFF needs 21 bits, 21 is not addressable, and the 11 left over are why Option<char> is still four bytes
Four lengths, and which one the other system means 201 One string, four counts — .len(), .chars().count(), encode_utf16().count() and the cluster count std refuses; which database, language and protocol means which; and the byte limit that panics mid-letter
Raw strings, escapes and the literal prefixes 101 → 201 Every way to write text in source — r"…" turns the escapes off, b"…" drops the UTF-8 promise, c"…" adds the NUL — and "C:\temp\new" is two bytes shorter than it looks
RFC 69 — how Rust got b'A' 201 Where that table came from — two pages in 2014, the alternatives it rejected, its three unresolved questions all since answered, and the pattern syntax that changed meaning underneath its one example
Walking a String 101 → 201 Three item types and the split family — splits are the gaps between matches, char_indices() is not chars().enumerate(), and split_whitespace() silently shortens a row
RFC 1212 — how lines() learned about \r\n 201 Why lines() handles Windows text — the 2015 change, the Unicode separators the thread argued for and refused, the trailing \r on a file's last line, and the round trip it quietly breaks
RFC 1054 — the method that renamed itself to promise less 201 Why words() is called split_whitespace() — std declining to define "a word", the pattern alternative that still will not compile, and the two definitions of whitespace std ships
Comparing and sorting text 201 Why "Zebra" < "apple" is true — Ord on a string is as_bytes().cmp(), so the order is lexicographic by code point; plus the ß that lowercasing both sides gets wrong, and why a language's alphabet is a table rather than an algorithm
Searching without splitting 101 → 201 Four questions and one trait — contains, find, starts_with and the split family take the same argument, find reports a byte offset, and Pattern is usable everywhere and nameable nowhere
Replacing part of a string 101 → 201 replace builds a new String rather than editing one — replace_range and retain are the two that really do, the chain that cannot swap two words, and the one pass that fixes it
Parsing out of a string 101 → 201 .parse() is a FromStr call, so the type comes from you and the failure comes back as a Result — the whitespace it never trims, the ASCII-only digits, and the kind() integers get and floats do not
The format mini-language 201 {:>8.3} is a language of its own — the whole grammar in nine lines, the precision that truncates a string instead of rounding it, and the {{:>8?}} that pads a number and ignores a &str
&'static str 201 On a literal it is the same type as &str — where the annotation starts refusing things, const vs static, and the three ways a String really can yield one
String parameters worth copying 201 → 301 Which signature makes the caller pay — &str by default, impl AsRef<str> for any kind of text, impl Into<String> when the function keeps it, Cow as a return — each counted in allocations
Six kinds of string 201 OsString, CString and friends are not five more inventions — three promises about the bytes, each owned or borrowed, and narrowing is where a promise gets checked
str is unsized 201 Why you never hold a str, only a pointer to one — the size that belongs to the value rather than the type, the fat pointer's second word, ?Sized as a relaxation, and the struct field that makes a whole struct unsized
Inside a Split 201 → 301 Why println!("{:?}", s.split(":")) prints a struct and not your pieces — the plan read field by field, the pattern that picks the searcher, and the one bool that is split_terminator
Splitting on nothing 201 Why "abc".split("") gives five pieces and not three — the empty pattern matches at every char boundary, the two counts an empty string produces, and the special case JavaScript, Go and Python each make instead
The third owned form 201 → 301 An owned string that is not a String — drop the capacity word and the handle shrinks to a &str's two words, the text stops being growable, and Rc<str> / Arc<str> give many owners one buffer, which is what makes a repeated column stop paying per row
str::as_str: the method that was stabilized and taken back 201 → 301 Why s.as_str() on a &str is E0658 and not "no such method" — what to write instead on Box<str>, Rc<str> and Cow<str>, why .as_ref() is the disputed answer, and the inherent-beats-trait rule that got the method reverted after it shipped
When the UTF-8 invariant broke 301 Three CVEs in the string library — str::repeat, String::retain and [Borrow<str>]::join — and one shape between them: std skipped a check because a String is always valid UTF-8, and safe code with no unsafe in it could make that stop being true
Wrong, but not unsafe 201 → 301 Two string bugs with no CVE and no crash — "bananas".contains("nana") was false, and a Greek word ending in Σ lowercased to the wrong sigma for seventeen releases — both of them in a fast path

The method reference

The lessons above teach the ideas. When you know which method you want and need to know how it behaves at the edges — what it panics on, what it allocates, which of the four pattern shapes it accepts — there is a page for every one:

reference pages what is in it
str methods 83 Everything about reading text: searching, splitting, trimming, case, parsing, and the byte-offset panics
String methods 42 Everything that needs ownership: building, growing, removing, capacity, and handing the allocation away

Every page carries the signature, the stability line, the trap the method is usually involved in, and a complete runnable program whose printed output is checked by CI — so nothing on them is a claim about what Rust does, only a record of what it did.

The split follows the types: because String dereferences to str, every str method works on a String unchanged, which is why the borrowed side is twice the size.

The lessons strings lean on

Strings are the worked example half the ownership pages already use, so the deep explanations live there and this section links them rather than moving them:

STRINGS.md is the full map: the same lessons with the question each one answers, plus the topics that are still outlines rather than lessons.

Strings: links, books and videos is the reading list — the Book, Programming Rust ch. 17, Easy Rust, the essays, and the exercise sets outside this library that map onto these pages.

The string crates is the other half of the outside world — fifteen crates for the jobs std declines, from graphemes and normalization to legacy encodings and strings that never touch the heap, each beside the lesson here that says what std gives you instead.

The bytes underneath are a library of their own. Everything this section takes as given — what UTF-8 is, why ż costs two bytes and an emoji four, and who checks that a run of bytes really is UTF-8 before from_utf8 will hand back a &str — is the subject of the sibling encodings learning library ↗, which teaches the same ground in Python, C and the shell alongside Rust. Validation is a boundary ↗ is the page that pairs with this chapter: it reads core's own validator, and shows what the &str invariant buys that a C char * and a Python str do not — the check runs once, so chars() afterwards does not run it again.

Po polsku

Tekst w Ruscie to dwa typy i jeden podział: String jest właścicielem swoich bajtów (łańcuch znaków, dane na stercie, można je rozbudowywać), a &str to tylko podgląd na cudzy tekst — wycinek łańcucha (string slice), czyli para „wskaźnik + długość”. Stąd praktyczna reguła całego działu: parametr funkcji bierze &str, bo chce tylko czytać, a pole struktury trzyma String, bo musi przeżyć wywołanie, które je stworzyło. Literał w kodzie nie leży ani na stosie, ani na stercie — siedzi w samym pliku wykonywalnym, dlatego ma typ &'static str.

Reszta niespodzianek bierze się z tego, że te bajty są w UTF-8 — i tu polski czytelnik ma trudniej niż angielski, bo trafia na nie od pierwszego dnia, a nie w egzotycznym przykładzie. Każde ą ć ę ł ń ó ś ź ż zajmuje dwa bajty, więc "żółw".len() daje 7, a nie 4. s[0] w ogóle się nie kompiluje (nie ma indeksowania po znakach), a &s[0..1] na słowie „żółw” kompiluje się znakomicie i panikuje w czasie działania: end byte index 1 is not a char boundary; it is inside 'ż' (bytes 0..2 of string). To jedyne miejsce w tym dziale, gdzie kompilator nie pilnuje za ciebie — dlatego do liczenia „liter” służy .chars().count(), a do cięcia w bezpiecznych miejscach char_indices(). Uwaga na skróty myślowe: char to jeden skalar Unicode, a nie „jedna litera na ekranie”, więc emoji ze znacznikiem koloru skóry to nadal kilka charów.

Sam dział jest zbudowany dwuwarstwowo i warto to wiedzieć, zanim zaczniesz szukać: lekcje (tabela na górze) tłumaczą pojęcia, a dokumentacja metod — 83 strony dla str i 42 dla String — odpowiada na pytanie „co ta metoda robi na brzegach, na czym panikuje, co alokuje”. Podział przebiega dokładnie po typach: String dereferencjonuje się do str, więc każda metoda str działa też na String i dlatego strona pożyczona jest dwa razy grubsza. Materiałów po polsku o łańcuchach jest niewiele — polskie tłumaczenie Tour of Rust kończy się na rozdziale 5, a tekst to rozdział 6 — więc do wyszukiwarki i tak wpisuje się angielskie hasła.

Szukaj po polsku: łańcuchy znaków w Ruscie · wycinek łańcucha · kodowanie UTF-8 a polskie znaki · rust String vs &str · rust byte index is not a char boundary