Sửa lỗi Rust: Tại sao String không thể được lập chỉ mục bằng usize

beginner🦀 Rust2026-07-01| Rust (mọi phiên bản), Cargo, Mọi hệ điều hành (Linux, macOS, Windows)

Error Message

error[E0277]: the type `String` cannot be indexed by `usize`
#rust#string#lập chỉ mục#kiểu dữ liệu#utf8

Vấn đề: Tại sao s[0] không hoạt động trong Rust

Nếu bạn chuyển từ Python, JavaScript hoặc C++, bản năng đầu tiên của bạn để lấy một ký tự có lẽ là my_string[0]. Trong Rust, hành động đó sẽ kích hoạt lỗi biên dịch ngay lập tức. Ngôn ngữ này đơn giản là không cho phép bạn làm điều đó.

error[E0277]: the type `String` cannot be indexed by `usize`
  --> src/main.rs:3:13
   |
 3 |     let c = s[0];
   |             ^^^^ `String` cannot be indexed by `usize`
   |
   = help: the trait `Index<usize>` is not implemented for `String`

Rust xử lý chuỗi ký tự (string) theo cách khác vì chúng là các trình bao bọc (wrapper) được mã hóa UTF-8 trên một Vec<u8>. Trong UTF-8, một ký tự đơn lẻ có thể chiếm từ 1 đến 4 bytes. Ví dụ, chữ cái 'R' là 1 byte, nhưng emoji con cua (🦀) là 4 bytes. Nếu Rust cho phép s[i], nó sẽ không biết bạn muốn lấy byte thứ i hay ký tự thứ i. Việc lập chỉ mục (indexing) byte thì nhanh (O(1)), nhưng lập chỉ mục ký tự yêu cầu phải duyệt qua toàn bộ chuỗi (O(n)). Rust buộc bạn phải rõ ràng để tránh những chi phí hiệu năng tiềm ẩn này.

Quy trình gỡ lỗi

Bạn có thể gặp phải vấn đề này khi cố gắng phân tích dữ liệu nhập từ người dùng. Hãy xem đoạn mã lỗi sau:

fn main() {
    let name = String::from("Rust");
    let first_letter = name[0]; // Dòng này làm hỏng quá trình build
    println!("First letter is: {}", first_letter);
}

Trình biên dịch đang chỉ ra rằng String không triển khai (implement) Index trait cho usize. Để tiếp tục, bạn phải chọn một chiến lược cụ thể dựa trên việc bạn cần tốc độ hay sự an toàn Unicode.

Giải pháp 1: Sử dụng .chars().nth() (Cách an toàn)

Nếu bạn cần một ký tự cụ thể và muốn xử lý Unicode chính xác, hãy sử dụng trình lặp (iterator) chars(). Đây là cách tiếp cận đáng tin cậy nhất cho hầu hết các ứng dụng.

fn main() {
    let name = String::from("Rust");
    
    // .chars() lặp qua các ký tự Unicode
    // .nth(0) cố gắng lấy ký tự đầu tiên một cách an toàn
    if let Some(first_char) = name.chars().nth(0) {
        println!("The first character is: {}", first_char);
    }
}

Ưu điểm: Nó xử lý các ký tự đa byte như "Ferris 🦀" mà không bị lỗi. Nhược điểm: Nó chạy trong thời gian O(n). Để tìm ký tự thứ 100, Rust phải xem xét mọi byte trước đó.

Giải pháp 2: Cắt chuỗi (Nhanh nhưng nguy hiểm)

Khi bạn chắc chắn 100% rằng ký tự của mình chỉ chiếm 1 byte (như mã ASCII tiêu chuẩn) hoặc bạn biết chính xác ranh giới các byte, bạn có thể sử dụng slice. Cách này trả về một &str thay vì một char.

fn main() {
    let name = String::from("Rust");
    let first_char_slice = &name[0..1]; 
    println!("First char: {}", first_char_slice);
}

Cảnh báo: Việc cắt (slicing) ở giữa một ký tự đa byte sẽ gây ra lỗi panic khi chạy. Chương trình của bạn sẽ bị sập ngay lập tức.

let crab = String::from("🦀");
let fail = &crab[0..1]; // PANIC! Emoji con cua dài 4 byte, và 1 không phải là một ranh giới.

Giải pháp 3: Truy cập Byte thô

Đôi khi bạn không quan tâm đến các ký tự mà chỉ cần giá trị u8 thô. Trong những trường hợp đó, hãy sử dụng as_bytes().

fn main() {
    let name = String::from("Rust");
    let first_byte = name.as_bytes()[0];
    println!("First byte value: {}", first_byte); // In ra 82, giá trị ASCII của 'R'
}

Xử lý Grapheme Clusters

Các ký tự hiển thị đôi khi có thể được tạo thành từ nhiều giá trị Unicode. Một emoji với tông màu da hoặc một chữ cái có dấu (như y̆) là những ví dụ điển hình. Đối với những trường hợp này, .chars() là không đủ. Bạn sẽ cần crate unicode-segmentation để xử lý các ký tự "theo cảm nhận của con người".

use unicode_segmentation::UnicodeSegmentation;

fn main() {
    let complex_emoji = "y̆"; // Đây là chữ 'y' cộng với một dấu breve kết hợp
    let first_grapheme = complex_emoji.graphemes(true).next().unwrap();
    println!("Real first char: {}", first_grapheme);
}

Xác minh: Cách xác nhận bản sửa lỗi

- Chạy `cargo check` để xác minh lỗi E0277 đã được giải quyết.
- Thực thi `cargo run` để xem kết quả đầu ra.
- Nếu bạn chọn `.nth()`, hãy kiểm tra với một chuỗi rỗng để đảm bảo việc xử lý `Option` của bạn hoạt động tốt.
- Nếu bạn chọn slicing, hãy thử truyền một ký tự không phải ASCII như `é` để đảm bảo ranh giới byte của bạn không gây ra panic.

Những bài học rút ra

- **Chuỗi không phải là mảng:** Rust ưu tiên tính toàn vẹn của dữ liệu hơn sự tiện lợi. Nó không cho phép bạn coi một chuỗi UTF-8 như một danh sách byte đơn giản trừ khi bạn yêu cầu rõ ràng.
- **Lưu ý độ phức tạp:** Truy cập ký tự thứ 500 là một thao tác O(n). Nếu bạn cần truy cập ngẫu nhiên thường xuyên, hãy cân nhắc lưu trữ dữ liệu của bạn trong một `Vec<char>` thay vì String.
- **An toàn là trên hết:** Hãy ưu tiên sử dụng `.chars().nth()` theo mặc định. Chỉ chuyển sang slicing hoặc truy cập byte nếu bạn đã đo lường được nút thắt cổ chai về hiệu năng và có thể đảm bảo ranh giới byte của mình.

Related Error Notes