How to iterate over the characters of a string
Short answer: text.chars() walks the characters one char at a time; text.bytes() walks the UTF-8 bytes one u8 at a time. A &str is UTF-8, so the two only agree on ASCII — "café" is 4 characters and 5 bytes. Use chars() when the text is the point, bytes() when the consumer wants bytes.
From a fresh project — cargo new chars-demo, then replace src/main.rs with each block below and run it with cargo run. The project name only shows up in cargo’s own output; nothing else on this page depends on it.
Characters, one at a time
Section titled “Characters, one at a time”fn main() { let text = "Ada Lovelace"; for c in text.chars() { println!("{c}"); }}Ada
LovelaceTwelve iterations for twelve characters, the space included — it prints as a line holding a single space. chars() is a method on str, so a String iterates the same way, and the items are char values: four bytes each in memory, holding one Unicode scalar value. Nothing is copied. Each char is read out of the buffer the &str borrows.
Bytes, one at a time
Section titled “Bytes, one at a time”fn main() { let text = "café"; println!("len() {}", text.len()); println!("chars {}", text.chars().count()); println!("bytes {}", text.bytes().count());}len() 5chars 4bytes 5len() is the byte length, not the character count — that’s the first place the difference bites. Every character that isn’t ASCII takes more than one byte:
fn main() { let text = "café"; for b in text.bytes() { println!("{b} = 0x{b:02x}"); }}99 = 0x6397 = 0x61102 = 0x66195 = 0xc3169 = 0xa9c, a and f are one byte each; é is two, 0xC3 0xA9, the UTF-8 encoding of U+00E9. An emoji takes four. bytes() yields the same bytes as as_bytes(), which hands over the whole &[u8] at once when the pieces aren’t needed one at a time.
A char is a scalar value, not a letter
Section titled “A char is a scalar value, not a letter”fn main() { for c in "café".chars() { println!("{c}: U+{:04X}", c as u32); }}c: U+0063a: U+0061f: U+0066é: U+00E9as u32 gives the code point, and char is exactly that: one Unicode scalar value. What it isn’t is a grapheme cluster — the thing a reader sees as one letter. An e followed by a combining acute accent is two chars and three bytes, and some emoji are several chars long. Counting characters and counting what a human sees are different jobs, and the second one needs grapheme segmentation, which has its own article later in this chapter.
Where each character sits
Section titled “Where each character sits”char_indices() pairs each character with the byte offset it starts at:
fn main() { let text = "aéb"; for (i, c) in text.char_indices() { println!("byte {i}: {c}"); }}byte 0: abyte 1: ébyte 3: bThe jump from 1 to 3 is é taking its two bytes. The offsets are byte positions, not character positions — the right unit for anything that needs to know where a character lives in the buffer, and the wrong unit for numbering the characters. Cutting a string at one of these offsets is the subject of the next article in this chapter.
Collecting the pieces
Section titled “Collecting the pieces”chars() collects into a Vec<char>, and back into a String:
fn main() { let text = "Ada Lovelace"; let letters: Vec<char> = text.chars().filter(|c| c.is_alphabetic()).collect(); println!("{} letters", letters.len()); let joined: String = text.chars().filter(|c| !c.is_whitespace()).collect(); println!("{joined}");}11 lettersAdaLovelaceString implements FromIterator<char>, so collecting characters gives text back. Collecting bytes doesn’t, because a u8 isn’t text until something has checked the encoding:
fn main() { let text = "café"; let rebuilt: String = text.bytes().collect(); println!("{rebuilt}");}error[E0277]: a value of type `String` cannot be built from an iterator over elements of type `u8` --> src/main.rs:3:40 |3 | let rebuilt: String = text.bytes().collect(); | ^^^^^^^ value of type `String` cannot be built from `std::iter::Iterator<Item=u8>` | = help: the trait `FromIterator<u8>` is not implemented for `String` = help: `String` implements trait `FromIterator<A>`: FromIterator<&char> FromIterator<&std::ascii::Char> FromIterator<&str> FromIterator<Box<str, A>> FromIterator<Cow<'_, str>> FromIterator<String> FromIterator<char> FromIterator<std::ascii::Char>note: the method call chain might not have had the expected associated types --> src/main.rs:3:32 |2 | let text = "café"; | ------ this expression has type `&str`3 | let rebuilt: String = text.bytes().collect(); | ^^^^^^^ `Iterator::Item` is `u8` herenote: required by a bound in `collect` --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/iter/traits/iterator.rs:2073:4
For more information about this error, try `rustc --explain E0277`.The compiler’s own list has FromIterator<char> in it and no u8 — the whole distinction on one screen. Bytes become text again through String::from_utf8, which checks the encoding as it converts and hands back an error when it isn’t valid UTF-8. That conversion has its own article later in this chapter.
Related
Section titled “Related”- What is the difference between String and &str? — the UTF-8 buffer both iterators walk
- How to split a string — iterating over pieces instead of characters
- How to build a string from parts — the other ways a
Stringgets built - Working with text — the rest of the chapter