Question
I am trying to index a string in Rust, but the compiler reports an error. Here is my code:
fn is_palindrome(num: u64) -> bool {
let num_string = num.to_string();
let num_length = num_string.len();
for i in 0..num_length / 2 {
if num_string[i] != num_string[(num_length - 1) - i] {
return false;
}
}
true
}
The compiler error is:
error[E0277]: the trait bound `std::string::String: std::ops::Index<usize>` is not satisfied
--> <anon>:7:12
|
7 | if num_string[i] != num_string[(num_length - 1) - i] {
| ^^^^^^^^^^^^^
|
= note: the type `std::string::String` cannot be indexed by `usize`
Why can a String not be indexed in Rust? How should I access its contents instead?
Short Answer
By the end of this page, you will understand why Rust does not allow String indexing with usize, how UTF-8 encoding affects string access, and which alternatives to use: bytes, chars(), slices, and iterators. You will also learn how to rewrite palindrome-style logic in an idiomatic Rust way.
Concept
In Rust, a String is not a simple array of characters. It is a growable UTF-8 encoded sequence of bytes.
That detail matters because UTF-8 characters do not all take the same number of bytes:
- Some characters use 1 byte, like
a - Some use 2, 3, or 4 bytes, like
é,中, or😊
Because of this, asking for string[3] is ambiguous:
- Do you mean the 4th byte?
- The 4th Unicode scalar value?
- The 4th user-visible grapheme cluster?
Rust avoids this ambiguity by not allowing direct integer indexing on String.
Why this matters
If Rust allowed String indexing like an array, code might accidentally land in the middle of a multi-byte UTF-8 character, producing invalid text or confusing behavior. By rejecting indexing, Rust forces you to be explicit about what you want:
- raw bytes with
.as_bytes() - Unicode scalar values with
.chars() - string slices with range syntax like
&s[start..end]when the indices are valid byte boundaries
This design improves safety and correctness, especially when handling non-English text.
Mental Model
Think of a Rust String like a sentence stored in a variable-width filing system.
With an array of integers, every item has the same size, so item 3 is easy to find.
With a UTF-8 string, each character can take a different amount of space, so the 4th character is not always at a predictable byte position.
It is like a bookshelf where some books are thin and some are thick:
- In a fixed-size system, "book 4" is always easy to locate.
- In a variable-size system, you must walk along the shelf and count properly.
Rust makes you choose how you want to walk:
- count bytes
- count characters
- count visible text units
Syntax and Examples
Accessing bytes
If you know your string contains only ASCII digits, like a number converted to text, accessing bytes is often the simplest solution.
fn is_palindrome(num: u64) -> bool {
let s = num.to_string();
let bytes = s.as_bytes();
for i in 0..bytes.len() / 2 {
if bytes[i] != bytes[bytes.len() - 1 - i] {
return false;
}
}
true
}
This works well here because decimal digits (0 to 9) are single-byte ASCII characters.
Accessing characters
If you want Unicode-aware character access, collect the characters first:
fn is_palindrome(s: &str) -> bool {
: <> = s.().();
..chars.() / {
chars[i] != chars[chars.() - - i] {
;
}
}
}
Step by Step Execution
Consider this version:
fn is_palindrome(num: u64) -> bool {
let s = num.to_string();
let bytes = s.as_bytes();
for i in 0..bytes.len() / 2 {
if bytes[i] != bytes[bytes.len() - 1 - i] {
return false;
}
}
true
}
Now trace it with num = 9009.
Step 1: Convert to string
let s = num.to_string();
s becomes:
"9009"
Step 2: Get bytes
Real World Use Cases
String access patterns matter in many real Rust programs.
Parsing input
When reading command-line arguments, files, or API payloads, you often need to:
- inspect bytes for protocols or file formats
- inspect characters for text processing
- slice strings safely for prefixes and tokens
Validation
Examples:
- checking whether a string starts with a digit
- validating a code, ID, or formatted input
- confirming a value contains only ASCII characters
Text processing
Examples:
- counting characters
- reversing text
- finding delimiters
- comparing text forwards and backwards
Working with structured formats
In CSV, JSON, log parsing, or custom text formats, developers often use:
.bytes()when working with raw encoded data.chars()when working with Unicode characters.lines(),.split(),.starts_with()and.contains()instead of manual indexing
Numeric string operations
Your palindrome example is common in coding challenges and utility scripts:
- convert a number to text
- compare both ends
Real Codebase Usage
In real Rust codebases, developers usually avoid manual string indexing entirely.
Common patterns
Use &str instead of String when borrowing
If a function only needs to read text, it usually takes &str:
fn is_palindrome(s: &str) -> bool {
s.chars().eq(s.chars().rev())
}
This is more flexible because it accepts both String and string literals.
Use bytes for ASCII-only logic
If the text is guaranteed to be ASCII, bytes are fast and simple:
fn is_numeric_palindrome(s: &str) -> bool {
let bytes = s.as_bytes();
bytes.iter().eq(bytes.iter().rev())
}
Common Mistakes
Mistake 1: Assuming len() means character count
Broken assumption:
let s = "é";
println!("{}", s.len()); // 2, not 1
Why it happens:
- Rust strings are UTF-8
len()returns bytes
How to avoid it:
- use
s.chars().count()when you need character count
Mistake 2: Trying to index a String like an array
Broken code:
let s = String::from("hello");
// let ch = s[0];
Why it fails:
Stringdoes not implementIndex<usize>
How to avoid it:
- use
s.as_bytes()[0]for bytes
Comparisons
| Approach | What it accesses | Can index directly? | Unicode-aware? | Typical use |
|---|---|---|---|---|
String | Owned UTF-8 text | No | Yes | Store and build text |
&str | Borrowed UTF-8 text | No | Yes | Read text efficiently |
as_bytes() | Raw bytes | Yes | No | ASCII-only logic, protocols, raw parsing |
chars() | Unicode scalar values | Not directly | Mostly | Character iteration |
Cheat Sheet
Quick rules
Stringis UTF-8 encoded textStringcannot be indexed withs[i]len()returns bytes, not characters- Use
.as_bytes()for byte-level access - Use
.chars()for character-level iteration - Use
collect::<Vec<char>>()if you truly need indexed character access - String slicing like
&s[a..b]uses byte offsets and must be on valid UTF-8 boundaries
Common patterns
let s = String::from("hello");
let bytes = s.as_bytes();
println!("{}", bytes[0]);
let s = "hello";
let first = s.chars().nth();
(, first);
FAQ
Why doesn't Rust allow string[0]?
Because Rust strings are UTF-8, and one character may use multiple bytes. Indexing by a single number would be ambiguous and unsafe.
How do I get the first character of a string in Rust?
Use:
let first = s.chars().next();
This returns an Option<char>.
How do I get the first byte of a string in Rust?
Use:
let first = s.as_bytes()[0];
Only do this when byte-level access is actually what you want.
Is len() the number of characters in a Rust string?
No. len() returns the number of bytes.
How can I index characters in Rust if I really need to?
Convert to a character vector:
let chars: Vec<char> = s.chars().();
= chars[];
Mini Project
Description
Build a small Rust utility that checks whether an input string is a palindrome in two ways: byte-based for ASCII-only input and character-based for general Unicode text. This project helps you practice when to use as_bytes() versus chars() and reinforces why direct String indexing is not allowed.
Goal
Create a program that safely checks palindromes without using string[index], using the right access method for the type of text being processed.
Requirements
- Write one function that checks ASCII palindromes using byte access.
- Write one function that checks general text palindromes using
chars(). - Print results for several sample inputs.
- Do not use direct indexing on
String. - Use
&strparameters for read-only string input.
Keep learning
Related questions
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Associated Types vs Generic Type Parameters in Rust: When to Use Each
Learn when to use associated types vs generic parameters in Rust traits, with clear rules, examples, and practical API design advice.
Can a Struct Extend Another Struct in Rust? Composition vs Inheritance
Learn how Rust handles struct reuse without inheritance, using composition, traits, and wrapper structs with practical examples.