Positions in a string
Every character in a string sits at a numbered position, counted from 0. In "Hello world!" the H is at position 0, the e at 1, and the space between the words at 5. Spaces and punctuation count like any other character.
Many languages let you write message[0] to read a character. Rust does not. The reason is how Rust stores text: a String or &str is a sequence of bytes in UTF-8, and a single character may take one to four of them. Position 3 in bytes is not always the fourth character, so a plain index has no single obvious meaning. This code does not compile:
let s = "Hello";
println!("{}", s[0]);
The compiler says the type `str` cannot be indexed by `{integer}` and suggests .chars().nth() or .bytes().nth(). Rust gives you several explicit ways to ask for a character instead, and each says exactly what it counts.
Option 1: collect the characters into a vector
chars() walks over the characters of a string, and collect() gathers them into a Vec<char>. A vector can be indexed with brackets, and the index counts characters:
let message = "Hello world!";
let chars: Vec<char> = message.chars().collect();
println!("{}", chars[0]);
println!("{}", chars[1]);
println!("{}", chars[6]);
H
e
w
This copies the whole string into a new vector, which is worth it when you access many positions. The type annotation Vec<char> tells collect() what to build. Indexing outside the vector panics.
Option 2: nth() on the iterator
For a single character, message.chars().nth(4) skips four characters and returns an Option<char>. It is Some('o') for this message and None when the position is past the end, so no panic happens until you call unwrap() on a None. The cost grows with the position, because nth has to walk from the start.
Option 3: a one-byte slice
A range in brackets slices a string by byte positions and borrows part of it. A range a..b includes a and excludes b, so 0..1 is exactly the first byte:
let fruit = "Avocado";
println!("{}", &fruit[0..1]);
println!("{}", &fruit[6..7]);
A
o
The result is a &str of length one, not a char. The & is required: the slice itself, fruit[0..1], is an unsized str that can't be stored or passed around directly, so you borrow it. This works well for text where every character is one byte, which is true of plain English letters, digits and common punctuation (ASCII).
Bytes and characters differ outside ASCII
Take the string "héllo". The letter é takes two bytes, so len() returns 6 while chars().count() returns 5. The character at position 1 is é through chars().nth(1), which gives Some('é'). A slice that cuts through the middle of that character, such as &s[0..2], panics at run time with a message that the byte index is not a char boundary. The checked method get(1..2) returns None in that case instead of panicking.
So pick the tool by what you are counting. Slices and len() count bytes and are fast, but only safe when you know the text is ASCII. chars() counts characters.
The lesson's exercise
The exercise declares fruit as the literal "Avocado" and prints two characters.
- The first character. The slice
&fruit[0..1]covers the byte at position 0. - The last character. The word has 7 bytes, so the last position is 6, and the slice
&fruit[6..7]picks it out.
let fruit = "Avocado";
println!("{}", &fruit[0..1]);
println!("{}", &fruit[6..7]);
Both slices are safe because every letter of "Avocado" is a single byte.
Common mistakes
- Writing
s[i]. It does not compile for strings; use a slice,chars()oras_bytes(). - Forgetting that the end of a range is excluded.
&fruit[0..1]is one character,&fruit[0..0]is empty. - Reading past the end.
&fruit[7..8]panics because the string has only 7 bytes. The safe formfruit.get(7..8)returnsNone. - Treating a slice as a
char. A slice is text; to get acharusechars().nth(i)or a vector.
Key takeaways
- Positions start at 0, and spaces count.
- Rust strings can't be indexed by a single integer, because characters vary in byte length.
Vec<char>andchars().nth(i)count characters;&s[a..b]counts bytes.- Prefer the checked
getornthwhen a position might be out of range.
