We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ground Rules: Values, Types, Control Flow step 24 of 24
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.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.