Kebiasaan dari bahasa lain membuat jari kita otomatis mengetik text[0] ketika ingin mengambil huruf pertama. Di PHP, Python, dan JavaScript akses semacam itu berjalan lancar, sehingga penolakan Rust terasa aneh: text[0] tidak sekadar menghasilkan hasil yang salah — kode tersebut bahkan tidak mau dikompilasi. Penolakan ini bukan sikap rewel. Teks dalam Rust disimpan sebagai byte UTF-8, dan di balik teks yang tampak sederhana ada tiga cara berbeda untuk menghitung “satu unit teks” — byte, character (code point), dan grapheme cluster. Chapter ini membedah ketiga wajah itu dan menunjukkan mengapa indeks angka tunggal terlalu ambigu untuk dipercaya.

Kesalahan yang Ditolak Sejak Kompilasi #

Mari kita tulis kode yang paling natural bagi pemula dan lihat apa kata kompilator:

rust
fn main() {
    let text = String::from("Halo");
    println!("huruf pertama: {}", text[0]);
}

Inti pesan error kompilator (sebagian keluaran dipangkas):

text
error[E0277]: the type `str` cannot be indexed by `{integer}`
 --> main.rs:3:40
  |
3 |     println!("huruf pertama: {}", text[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>

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.

Pesan E0277 ini padat informasi. Perhatikan tiga hal: pertama, meski variabelnya bertipe String, pesan menyebut tipe str — pengindeksan teks selalu berujung pada lapisan teks mentah di dalamnya. Kedua, satu-satunya “indeks” yang diterima teks adalah range usize seperti 0..3 — pola slice yang akan kita pakai di Chapter Memotong dan Menggabungkan String. Ketiga, kompilator menyarankan .chars().nth() atau .bytes().nth() sebagai pintu resmi mengambil elemen ke-n. Rust menutup jalan yang membingungkan sambil menunjukkan dua jalan yang jelas.

Tiga Wajah Teks yang Sama #

Untuk memahami ambiguitasnya, kita perlu melihat bagaimana UTF-8 menyimpan teks. UTF-8 adalah encoding dengan panjang variabel: karakter ASCII memakan 1 byte, karakter seperti é atau huruf Rusia memakan 2 byte, banyak huruf Asia 3 byte, dan emoji 4 byte. Sebuah String hanyalah barisan byte hasil encoding tersebut — dan “jumlah unit teks” bergantung pada kacamata yang kita pakai:

  • Byte: satuan penyimpanan mentah. Inilah yang dihitung len().
  • Character (code point): satuan hasil decode UTF-8. Inilah yang dihitung chars().count().
  • Grapheme cluster: satuan yang dilihat manusia. “é” yang ditulis dengan huruf dasar plus aksen tetap satu grapheme cluster meski terdiri dari dua character.

Mari kita adukan ketiga kacamata pertama pada beberapa teks:

rust
fn main() {
    let greeting = "Halo";
    let cafe = "café";
    let russian = "Привет";
    let family = "👨‍👩‍👧‍👦";
    println!("Halo: {} byte, {} character", greeting.len(), greeting.chars().count());
    println!("café: {} byte, {} character", cafe.len(), cafe.chars().count());
    println!("Привет: {} byte, {} character", russian.len(), russian.chars().count());
    println!("emoji keluarga: {} byte, {} character", family.len(), family.chars().count());
}

Output yang Dihasilkan:

text
Halo: 4 byte, 4 character
café: 5 byte, 4 character
Привет: 12 byte, 6 character
emoji keluarga: 25 byte, 7 character

Mari kita bedah kode dan hasilnya:

  1. Baris 2-5 (let greeting = ...; dst.): empat teks disimpan sebagai literal &str — representasi byte UTF-8 di binary, sama seperti isi String.
  2. Baris 6 (println!("Halo: ...", greeting.len(), greeting.chars().count());): len() menghitung byte; chars() membagi teks menjadi barisan character lalu count() menghitung jumlahnya (iterasi ini kita bahas tuntas di Chapter Iterasi String). Untuk ASCII murni seperti Halo, byte dan character berimpit: 4 dan 4.
  3. Baris 7 (café): é memakan 2 byte, jadi 5 byte namun tetap 4 character.
  4. Baris 8 (Привет): keenam huruf Rusia masing-masing 2 byte — 12 byte untuk 6 character.
  5. Baris 9 (emoji keluarga 👨‍👩‍👧‍👦): emoji ini dirangkai dari empat emoji orang yang disambung tiga zero-width joiner — total 7 character dan 25 byte. Namun mata manusia melihat satu unit: satu grapheme cluster.

Kalau Bisa Diindeks, Byte Apa yang Dikembalikan #

Kita bisa membayangkan akibatnya lewat pinjaman byte mentah. Metode as_bytes() meminjang isi teks sebagai barisan byte, dan barisan byte memang boleh diindeks — fitur array yang sudah kita kenal di Section Dasar Sintaks:

rust
fn main() {
    let text = "Halo";
    let byte_values = text.as_bytes();
    println!("byte pertama: {}", byte_values[0]);
    println!("byte pertama sebagai char: {}", byte_values[0] as char);
}

Output yang Dihasilkan:

text
byte pertama: 72
byte pertama sebagai char: H

Bedah singkatnya:

  1. Baris 2 (let text = "Halo";): text adalah literal berisi 4 byte UTF-8.
  2. Baris 3 (let byte_values = text.as_bytes();): as_bytes() meminjang teks sebagai barisan u8 tanpa menyalin apa pun — byte_values melihat byte yang sama dengan yang dilihat text.
  3. Baris 4 (println!("byte pertama: {}", byte_values[0]);): indeks 0 pada barisan byte mengembalikan 72 — kode numerik huruf H — bukan hurufnya.
  4. Baris 5 (byte_values[0] as char): konversi as char mengubah angka 72 menjadi character H. Baris ini sengaja memperlihatkan jarak antara byte dan character.

Inilah argumen pertama penolakan indeks: text[0] pada café akan mengembalikan angka tanpa makna di tengah encoding é. Argumen keduanya lebih teknis: UTF-8 berpanjang variabel, jadi posisi character ke-n tidak bisa dihitung dengan perkalian — menemukannya menuntut pemindaian byte dari awal, operasi berbiaya linear. Indeks yang tampak sekejap di bahasa lain sebenarnya menyembunyikan pemindaian mahal atau pemeriksaan batas yang bisa gagal. Rust memilih jujur: biaya yang nyata seharusnya tampak nyata.

Jalan Keluar yang Dianjurkan #

Kompilator sudah menyebutkannya di pesan error: .chars().nth() mengambil character ke-n dan .bytes().nth() mengambil byte ke-n — keduanya memindai dari depan sesuai biaya aslinya, dan keduanya mengembalikan nilai bertipe Option yang mengingatkan bahwa posisi yang diminta bisa saja melebihi panjang teks (tipe Option kita bahas di Section Struktur Data). Untuk kebutuhan yang lebih luas, dua Chapter berikutnya menyediakan pintu resmi: iterasi character demi character untuk memproses seluruh teks, dan slice rentang byte untuk mengambil potongan dengan batas yang diperiksa.

Ringkasan #

Teks Rust adalah byte UTF-8, dan satu teks yang sama bisa dihitung sebagai byte, character, maupun grapheme cluster — tiga angka yang hanya berimpit pada teks ASCII murni. Indeks tunggal seperti text[0] terlalu ambigu untuk ketiganya sekaligus, sehingga E0277 menutupnya sejak kompilasi dan mengarahkan kita ke .chars().nth(), .bytes().nth(), atau slice rentang usize. Barisan byte mentah tetap bisa diintip lewat as_bytes() — dan dari sana kita melihat bahwa indeks 0 mengembalikan angka 72, bukan huruf. Membaca teks demi teks dengan sadar adalah langkah berikutnya: Chapter Iterasi String membuka kotak chars() dan bytes().