Skip to content

str::chars

str methods · Strings

Level: reference · for working programmers

One line: An iterator over Unicode scalar values, yielding char — the closest thing Rust has to 'the characters', and still not what a reader would call one.

pub fn chars(&self) -> Chars<'_>

Stable since 1.0.0.

chars() decodes UTF-8 on the fly, so each item is a whole char however many bytes it took to store. That makes it the right iterator for almost everything: counting, filtering, mapping, reversing.

It is O(n) to count, and there is no s[i] shortcut, because finding character i means decoding the i before it. s.chars().nth(i) says that out loud.

A char is a Unicode scalar, not a grapheme. What a person points at as one character can be several: é may be one scalar or two (e + a combining accent), and a flag or a family emoji is several on purpose. So chars().count() is not "how many letters", and chars().rev() can reorder a combining mark away from the letter it belongs to. When you need what a reader sees, that is grapheme segmentation, and it lives outside std in the unicode-segmentation crate.

For the byte offsets you can slice at, use char_indiceschars().enumerate() gives ordinals, which are not offsets.

Example

str_chars.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.

fn main() {
    let s = "héllo";

    println!("{:?}", s.chars().collect::<Vec<char>>());
    println!("{} chars from {} bytes", s.chars().count(), s.len());

    // The whole iterator toolkit applies.
    println!("{}", s.chars().filter(|c| c.is_alphabetic()).count());
    println!("{}", s.chars().rev().collect::<String>());
    println!("{}", s.chars().map(|c| c.to_ascii_uppercase()).collect::<String>());

    // There is no s[i]; nth() decodes its way there, and says so by being O(n).
    println!("{:?}", s.chars().nth(1));

    // A scalar is not a grapheme: same visible text, two different counts.
    let precomposed = "é";           // U+00E9
    let combining = "e\u{301}";      // 'e' + COMBINING ACUTE ACCENT
    println!("{} vs {} chars, printed {} and {}",
             precomposed.chars().count(), combining.chars().count(),
             precomposed, combining);
}

Verified output of str_chars.rs — regenerated by tools/run_examples.py, never hand-typed.

['h', 'é', 'l', 'l', 'o']
5 chars from 6 bytes
5
olléh
HéLLO
Some('é')
1 vs 2 chars, printed é and é

See also

str::chars in the standard library ↗

Po polsku

Polskie słowo „znak” jest szersze niż char: char to skalar Unicode, a nie to, co czytelnik pokazuje palcem jako jedną literę. Zwykle się to nie rozjeżdża, bo w postaci znormalizowanej NFC każda polska litera — ą, ć, ę, ł, ń, ó, ś, ź, ż — jest jednym skalarem i "żółw".chars().count() daje 4. Ale ten sam tekst w NFD (tak bywa z tekstem skopiowanym z macOS-a albo wyjętym z bazy o innej normalizacji) rozkłada się na literę bazową plus znak łączący, więc ta sama funkcja odpowie 6, a chars().rev() oderwie wtedy diakrytyk od jego litery i doklei go do sąsiedniej. Domyka to jeden ładny szczegół: ł jako jedyna z tych liter nie ma rozkładu kanonicznego i w NFD zostaje sobą.

Kiedy naprawdę chodzi o „to, co widzi czytelnik”, potrzebna jest segmentacja na grafemy — w std jej nie ma, mieszka w crate'cie unicode-segmentation. I jeszcze jedno przyzwyczajenie z Pythona warte porzucenia: s[i] tutaj nie istnieje, bo żeby dojść do znaku i, trzeba zdekodować wszystkie wcześniejsze; s.chars().nth(i) mówi to wprost i jest O(n).

Szukaj po polsku: skalar Unicode a grafem · normalizacja NFC NFD · znaki łączące · rust str chars · rust unicode-segmentation graphemes