Topik ini membahas dua tipe teks Rust, yaitu String yang memiliki penyangga byte milik sendiri dan &str yang hanya meminjam tampilan atas teks. Pembahasan mencakup pembuatan dan penggabungan String, tata memori UTF-8 yang membuat panjang teks dihitung dalam byte, alasan teks tidak dapat diindeks satu karakter, pemilihan tipe parameter di antara keduanya, serta validasi barisan byte yang tidak sah. Setelah membaca topik ini, tipe teks dapat dipilih sesuai kepemilikan datanya, dan perilaku teks non-ASCII tidak lagi mengejutkan.
String sebagai teks milik sendiri#
Sebuah String adalah pembungkus atas Vec<u8> yang isinya dijamin berupa barisan byte UTF-8 yang sah, sehingga teks bertipe ini dapat bertambah, dipotong, dan disimpan selama program membutuhkan. Nilainya dibuat dari literal dengan String::from atau dengan metode to_string pada &str, kemudian diisi lebih lanjut dengan push_str yang menambah potongan teks dan push yang menambah satu karakter. Operator + menggabungkan dua teks dengan satu catatan kepemilikan: operan kirinya dipindahkan ke hasil, sedangkan operan kanannya hanya dipinjam lewat referensi. Makro format! menggabungkan banyak bagian tanpa memindahkan operan apa pun karena hasilnya dibangun dari nol.
fn main() {
let mut teks = String::from("suhu");
println!("awal : {teks}");
teks.push_str(" ruangan");
println!("push_str : {teks}");
teks.push('!');
println!("push : {teks}");
let satuan = String::from(" naik");
let gabung = teks + &satuan; // teks dipindahkan ke hasil gabung
println!("gabung : {gabung}");
let laporan = format!("{gabung} (pagi ini)");
println!("format! : {laporan}");
}awal : suhu
push_str : suhu ruangan
push : suhu ruangan!
gabung : suhu ruangan! naik
format! : suhu ruangan! naik (pagi ini)Keluaran tersebut memperlihatkan pertumbuhan teks tahap demi tahap. Setelah baris gabung, variabel teks tidak dapat dipakai lagi karena isinya telah dipindahkan oleh operator +, sedangkan satuan tetap utuh karena hanya dipinjam melalui &satuan. Makro format! menangkap gabung hanya untuk dibaca, sehingga variabel itu tetap hidup sesudahnya. Untuk penggabungan lebih dari dua bagian, format! umumnya lebih mudah dibaca daripada rantai + yang menumpuk pemindahan, dan makro ini juga menerima angka serta tipe lain yang mengimplementasikan trait Display.
String adalah Vec berisi UTF-8#
Isi String adalah barisan byte UTF-8, bukan barisan karakter. Encoding UTF-8 memakai satu byte untuk karakter ASCII seperti huruf dan angka, tetapi karakter di luarnya menempati dua sampai empat byte. Akibatnya, metode len() melaporkan jumlah byte, sedangkan jumlah karakter dihitung dengan chars().count(), dan kedua angka itu hanya sama apabila seluruh karakter teksnya ASCII. Tanda derajat ° pada teks 23°C adalah contoh yang lazim pada data sensor: karakter itu menempati dua byte, sehingga teksnya panjang lima byte meskipun jumlah karakternya empat.
fn main() {
let polos = "23C";
println!("\"{}\": {} byte, {} karakter", polos, polos.len(), polos.chars().count());
let derajat = "23°C";
println!("\"{}\": {} byte, {} karakter", derajat, derajat.len(), derajat.chars().count());
}"23C": 3 byte, 3 karakter
"23°C": 5 byte, 4 karakterDiagram berikut memetakan teks 23°C ke penyangga byte di heap. Struktur kendali String di stack berupa penunjuk, panjang, dan kapasitas, sama seperti pada Vec, dan tanda derajat menempati dua sel byte yang berdampingan.
Karakter tidak diakses dengan indeks#
Kebiasaan mengambil karakter pertama dengan teks[0] tidak berlaku pada teks Rust, dan penolakannya terjadi saat kompilasi. Satu byte pada posisi tertentu belum tentu satu karakter yang utuh, dan satu karakter dapat menempati beberapa byte, sehingga bentuk akses per karakter tidak memiliki makna yang tunggal. Bahasa ini memilih menolak pengindeksan bilangan pada teks sama sekali daripada menebak makna mana yang diinginkan. Penolakan tersebut dapat diperiksa langsung dengan program kecil berikut.
export PATH="$HOME/.cargo/bin:$PATH"
# buat program yang mencoba mengakses satu karakter dengan indeks bilangan
printf 'fn main() {\n let teks = String::from("suhu");\n println!("{}", teks[0]);\n}\n' > salah-indeks.rs
# kompilasi ditolak karena teks tidak dapat diindeks oleh bilangan
rustc --edition=2021 salah-indeks.rs 2>&1 | sed -n '1,8p'error[E0277]: the type `str` cannot be indexed by `{integer}`
--> salah-indeks.rs:3:25
|
3 | println!("{}", teks[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()`Error E0277 menyatakan bahwa tipe str tidak dapat diindeks oleh bilangan, dan catatannya menyarankan chars().nth() atau bytes().nth() sebagai pengganti. Untuk membaca isi teks, dua iterator tersedia: chars() menghasilkan karakter demi karakter bertipe char, sedangkan bytes() menghasilkan byte demi byte bertipe u8. Program berikut memakai keduanya pada teks berisi tanda derajat.
fn main() {
let teks = "23°C";
for (i, c) in teks.chars().enumerate() {
println!("karakter {}: {}", i, c);
}
let byte: Vec<u8> = teks.bytes().collect();
println!("byte : {:?}", byte);
let ukuran: Vec<usize> = teks.chars().map(|c| c.len_utf8()).collect();
println!("byte per karakter : {:?}", ukuran);
}karakter 0: 2
karakter 1: 3
karakter 2: °
karakter 3: C
byte : [50, 51, 194, 176, 67]
byte per karakter : [1, 1, 2, 1]Iterasi chars() menghasilkan empat karakter dengan ° utuh pada indeks dua, sedangkan iterasi bytes() menghasilkan lima byte. Pasangan 194 dan 176 adalah bentang dua byte karakter °, dan metode len_utf8() merangkum hal yang sama dalam angka. Angka pada keluaran pertama adalah indeks karakter, bukan indeks byte, dan indeks byte tempat karakter ° bermula juga bernilai dua karena dua karakter sebelumnya masing-masing menempati satu byte. Pemrosesan teks yang sadar karakter selalu berjalan lewat chars(), sedangkan bytes() dipakai untuk protokol biner atau teks yang dijamin ASCII.
&str: teks yang dipinjam#
&str adalah irisan teks yang hanya meminjam isinya tanpa memiliki penyangga, seperti &[T] terhadap deret data. Literal teks bertipe &str dan menunjuk data yang tertanam pada berkas biner program, sedangkan &String dapat dipakai sebagai &str melalui koersi deref. Fungsi yang menerima &str karenanya melayani literal, irisan, maupun referensi ke String sekaligus, dan bentuk parameter inilah yang menjadi kebiasaan umum pada API Rust. Kepemilikan String cukup disimpan pada satu tempat penyimpanan data, misalnya field struct atau variabel pemilik utama.
fn sapa(nama: &str) -> String {
format!("Selamat datang, {nama}!")
}
fn main() {
let pengguna = String::from("Citra");
println!("{}", sapa(&pengguna));
println!("{}", sapa("Dewi"));
}Selamat datang, Citra!
Selamat datang, Dewi!Fungsi sapa menerima nama sebagai &str dan mengembalikan String baru yang dibangun oleh format!. Pemanggilan pertama mengirim &pengguna yang berkoersi dari &String menjadi &str, sedangkan pemanggilan kedua mengirim literal bertipe &str tanpa perantara. Arah pemilihannya sederhana: terima &str pada parameter apabila isi fungsi cukup membaca teks, dan kembalikan String apabila hasil fungsi memang teks baru yang harus dimiliki pemanggilnya. Penyimpanan jangka panjang, misalnya field struct, memakai String agar data tidak bergantung pada pemilik lain.
Validasi UTF-8#
Jaminan bahwa String selalu berisi UTF-8 yang sah ditegakkan pada dua gerbang. Gerbang pertama adalah literal pada kode sumber: penulisan \xC3\x28 pada literal teks ditolak saat kompilasi karena escape byte pada rentang di atas 0x7F hanya sah pada literal byte, bukan literal teks. Gerbang kedua adalah data dari luar program, misalnya isi berkas atau hasil pembacaan jaringan, yang baru dapat diperiksa saat program berjalan melalui String::from_utf8. Fungsi tersebut menerima Vec<u8> dan mengembalikan Result: varian Ok membawa String yang sah, sedangkan varian Err membawa error yang menunjuk byte penyebabnya.
export PATH="$HOME/.cargo/bin:$PATH"
# buat program berisi barisan byte yang bukan UTF-8 yang sah
printf 'fn main() {\n let teks = String::from("\\xC3\\x28");\n println!("{teks}");\n}\n' > salah-utf8.rs
# kompilasi ditolak karena literal teks wajib berupa UTF-8 yang sah
rustc --edition=2021 salah-utf8.rs 2>&1 | sed -n '1,8p'error: out of range hex escape
--> salah-utf8.rs:2:30
|
2 | let teks = String::from("\xC3\x28");
| ^^^^ must be a character in the range [\x00-\x7f]
error: aborting due to 1 previous errorEscape \xC3 ditolak karena harus berada pada rentang \x00 sampai \x7f, dan rentang itu memang dibatasi agar literal teks selalu menjadi UTF-8 yang sah. Karakter non-ASCII ditulis langsung seperti °, atau lewat escape Unicode seperti \u{00B0} untuk tanda derajat. Pemeriksaan pada data yang datang dari luar program memakai String::from_utf8 sebagai berikut.
fn main() {
let dari_byte = String::from_utf8(vec![0x32, 0x33]);
println!("{:?}", dari_byte);
let rusak = String::from_utf8(vec![0xC3, 0x28]);
match rusak {
Ok(valid) => println!("valid : {valid}"),
Err(_) => println!("byte tidak valid ditolak"),
}
println!("versi lossy : {}", String::from_utf8_lossy(&[0xC3, 0x28]));
}Ok("23")
byte tidak valid ditolak
versi lossy : �(Dua byte 0x32 dan 0x33 membentuk teks 23 yang sah sehingga hasilnya Ok("23"). Barisan 0xC3 dan 0x28 bukan UTF-8 yang sah, sehingga from_utf8 mengembalikan Err yang ditangani cabang kedua. Varian from_utf8_lossy tetap menghasilkan String dengan mengganti bagian yang rusak dengan karakter pengganti, dan keluarannya memperlihatkan karakter pengganti tersebut diikuti tanda kurung yang lolos pemeriksaan. Pilihan di antara keduanya adalah pilihan antara menolak data yang rusak dan menyelamatkan sebagian isinya.
Kesalahan umum#
Menghitung batas tampilan dengan jumlah karakter. Perhitungan yang menganggap satu karakter sebagai satu byte menghasilkan batas byte yang salah untuk teks non-ASCII, dan irisan dengan batas seperti itu menghentikan program karena memotong tengah karakter. Hitung batas melalui char_indices() atau find() yang bekerja pada karakter, atau proses teks lewat chars() tanpa irisan sama sekali.
Menggabungkan banyak bagian dengan rantai operator +. Setiap operator + memindahkan operan kirinya, sehingga rantai a + &b + &c menghabiskan a dan menuntut referensi pada sisanya. Makro format! menghasilkan teks yang sama tanpa memindahkan operan mana pun, dengan placeholder yang juga lebih terbaca ketika bagian-bagiannya banyak.
Ringkasan#
StringadalahVec<u8>berisi UTF-8 yang sah dengan kepemilikan penuh, sedangkan&stradalah irisan teks yang dipinjam dan mencukupi untuk mayoritas parameter fungsi.len()menghitung byte,chars().count()menghitung karakter, dan keduanya hanya bernilai sama untuk teks yang seluruh karakternya ASCII.- Pengindeksan bilangan pada teks ditolak saat kompilasi, sehingga pembacaan isi dikerjakan lewat
chars(),bytes(), atau irisan dengan batas byte yang sah. - Operator
+memindahkan operan kirinya,format!menggabungkan tanpa memindahkan operan, danString::from_utf8memeriksa byte dari luar program saat berjalan.
Referensi#
- Storing UTF-8 Encoded Text with Strings — The Rust Programming Language
— Bab resmi tentang
String,&str, dan alasan teks tidak diindeks oleh bilangan. - Struct String — Rust standard library
— Dokumentasi
Stringbesertafrom_utf8danfrom_utf8_lossy. - Primitive type str — Rust standard library
— Dokumentasi tipe
strbeserta metode iterasi karakter dan byte.