Skip to content

← Ground Rules: Values, Types, Control Flow step 24 of 24

Hard Primitives

Strings are UTF-8: bytes, chars, and the indexing panic

Report three facts about a string: its length in bytes, its length in characters, and itself reversed character by character.

pub fn char_stats(text: &str) -> (usize, usize, String)

"héllo" gives (6, 5, "olléh") — six bytes, five characters, because é is two bytes of UTF-8. "" gives (0, 0, ""). "abc" gives (3, 3, "cba"), where the two lengths agree because everything is ASCII.

The starter does not compile, and the error is the most conspicuous place in the language where Rust refuses to let you be subtly wrong.

s[0] is a compile error, not a panic

let s = "héllo";
let c = s[0];
error[E0277]: the type `str` cannot be indexed by `{integer}`
   = help: the trait `Index<{integer}>` is not implemented for `str`
   = note: you can use `.chars().nth()` or `.as_bytes()[n]` instead

Every other mainstream language lets you index a string, and every one of them is quietly broken on non-ASCII input — either handing you a byte and calling it a character, or handing you half of a UTF-16 surrogate pair. Rust does not implement Index<usize> for str at all, so the mistake cannot be made.

The refusal is honest, because there is no good answer. If s[0] gave a byte, it would be useless for text. If it gave a character, it would be O(n) — you cannot find the k-th character of a UTF-8 string without scanning from the start, because characters are one to four bytes long. Rust will not silently hide an O(n) operation behind O(1) syntax.

Range slicing compiles, and then panics

Distinguish these two carefully; they fail at different times:

s[0]       // does not compile           — E0277
&s[0..2]   // compiles, panics at run time on "héllo"

str does implement Index<Range<usize>>, because slicing a run of bytes out of a string is a real and useful operation — as long as both ends land on character boundaries. When they do not, you get:

thread 'main' panicked at src/main.rs:3:14:
byte index 2 is not a char boundary; it is inside 'é' (bytes 1..3) of `héllo`

That is a genuinely excellent error message: it names the byte, the character it landed inside, and the byte range that character actually occupies. If you ever see it, you were treating byte offsets as character offsets.

Safe alternatives: s.get(0..2) returns Option<&str> and gives you None instead of a panic; s.char_indices() yields (byte_offset, char) pairs so you can slice at offsets you know are valid; s.is_char_boundary(i) asks directly.

The three views of a string

text.len()            // usize — bytes. O(1). It is stored.
text.chars().count()  // usize — Unicode scalar values. O(n). It is counted.
text.bytes()          // iterator of u8
text.chars()          // iterator of char
text.char_indices()   // iterator of (usize, char)

len() being bytes is not a wart. It is the length of the buffer, it is what every allocation and slice bound is measured in, and making it O(1) is why String is a usable data structure. The character count is a different question with a different cost, and Rust makes you ask it separately.

Two default-on clippy lints police the beginner shortcuts here: iter_nth_zero catches s.chars().nth(0), which should be s.chars().next(); and chars_next_cmp catches s.chars().next() == Some('x'), which should be s.starts_with('x').

Reversing

text.chars().rev().collect() reverses by character and is correct for every test here. Reversing by byte would corrupt every multi-byte character in the string, which is what the starter’s loop was heading toward.

Be honest about the limit, though: reversing by char is still not correct in general. A “grapheme cluster” — what a human calls a character — can be several scalar values: e plus a combining acute accent, a flag made of two regional indicators, an emoji with a skin-tone modifier. Reversing by char splits those apart.

The standard library cannot do graphemes. Doing it properly requires the Unicode segmentation tables, which live in the unicode-segmentation crate — and this course has no crates. So: know that chars().rev() is the right answer for this problem and the wrong answer for a text editor, and know why.