Skip to content
Select theme

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.

A
d
a
L
o
v
e
l
a
c
e

Twelve 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.

len() 5
chars 4
bytes 5

len() 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:

99 = 0x63
97 = 0x61
102 = 0x66
195 = 0xc3
169 = 0xa9

c, 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.

c: U+0063
a: U+0061
f: U+0066
é: U+00E9

as 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.

char_indices() pairs each character with the byte offset it starts at:

byte 0: a
byte 1: é
byte 3: b

The 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.

chars() collects into a Vec<char>, and back into a String:

11 letters
AdaLovelace

String 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:

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` here
note: 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.