Skip to content
Select theme

Why can't you index into a String?

Short answer: a String is a buffer of UTF-8 bytes, and a character is one to four of them, so s[0] can’t say whether 0 means a byte or a character — the compiler refuses instead of guessing. Vec indexing works because every element is the same size; a string has no fixed stride to multiply by. Write s.chars().next() for the first character, s.as_bytes()[0] for the first byte.

From a fresh project — cargo new index-demo, replace src/main.rs with the block below, then cargo build.

error[E0277]: the type `str` cannot be indexed by `{integer}`
--> src/main.rs:3:15
|
3 | let c = s[0];
| ^ string indices are ranges of `usize`
|
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`
= note: you can use `.chars().nth()` or `.bytes().nth()`
for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
help: `usize` implements trait `SliceIndex<T>`
--> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/slice/index.rs:179:0
|
= note: `SliceIndex<[T]>`
--> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/bstr/traits.rs:197:0
|
= note: `SliceIndex<ByteStr>`
= note: required for `String` to implement `Index<{integer}>`
For more information about this error, try `rustc --explain E0277`.

The headline is the rule, and it says str even for a String: indexing is looked up through the Deref to str, and the closing note, “required for String to implement Index<{integer}>”, spells out that chain. The label under the code is the rule itself: string indices are ranges of usize. Number indexing doesn’t exist; range indexing does — the slicing the previous article covers. A &str gives the same error.

“café” is five bytes and four characters: s.len() is 5, s.chars().count() is 4. The é at the end is two bytes, 195 and 169, so the fourth character starts at byte 3. Ask for s[3] and there are two defensible answers: byte 3, half of the é, or character 3, the é itself. No arithmetic turns one into the other: finding the n-th character means walking from the start, reading each character’s width before the next one’s position is known. The compiler’s note points at .chars().nth() and .bytes().nth() — methods that walk.

  • s.chars().nth(n) — the n-th character, an Option<char>: Some('é') for the fourth character of “café”. Each call walks from the start; a loop over chars() beats nth per index.
  • s.chars().next() — the first character, without the walk.
  • s.as_bytes()[n] — the n-th byte, a u8: 99 for “café”. Only meaningful when the text is known to be ASCII; elsewhere a byte can be half a character.
  • &s[a..b] — a range of bytes, the indexing that does exist, under the char-boundary rules from the slicing article.