String literal memberi kita teks yang beku: isinya sudah ditentukan saat menulis kode, ukurannya tetap, dan tidak ada seorang pun yang boleh mengubahnya. Program sungguhan jarang sepatuh itu — nama pengguna baru diketahui saat program berjalan, isi berkas dibaca saat dibutuhkan, dan kalimat sering harus dirangkai potong demi potong. Kebutuhan itu ditangani tipe String: teks yang tersimpan di heap, dapat tumbuh dan menyusut, serta dimiliki penuh oleh satu pemilik yang bertanggung jawab membebaskannya. Chapter ini membedah String dari sisi mesin — apa yang ada di stack, apa yang ada di heap, dan bagaimana pertumbuhan teks dibiayai — sehingga keputusan memilih antara &str dan String tidak lagi ditebak.
Kepemilikan Penuh atas Teks #
String adalah teks milik sendiri. Berbeda dari &str yang hanya menatap, sebuah String mengalokasikan buffer byte di heap, mengisi byte UTF-8 ke sana, dan — sesuai aturan ownership dari Section sebelumnya — menjadi pemilik tunggal buffer tersebut. Ketika pemilik keluar dari scope, buffer di heap ikut dibebaskan. Di balik layar, String dibangun di atas wadah byte yang dapat tumbuh (jenis Vec, yang akan kita bahas tuntas di Section Koleksi) dengan satu jaminan tambahan: isinya selalu UTF-8 yang sah.
Membuat String dapat dilakukan dengan beberapa pintu masuk. Dua yang paling sering dipakai adalah String::from yang mengubah literal menjadi teks milik sendiri, dan metode .to_string() milik literal yang hasilnya sama:
fn main() {
let from_literal = String::from("Budi Santoso");
let converted = "Siti Aminah".to_string();
let empty = String::new();
println!("{}", from_literal);
println!("{}", converted);
println!("panjang teks kosong: {}", empty.len());
}Output yang Dihasilkan:
Budi Santoso
Siti Aminah
panjang teks kosong: 0Mari kita bedah kode di atas:
- Baris 2 (
let from_literal = String::from("Budi Santoso");):String::frommembaca byte literal, mengalokasikan buffer baru di heap, lalu menyalin byte tersebut ke buffer.from_literalmenjadi pemilik buffer itu. - Baris 3 (
let converted = "Siti Aminah".to_string();):.to_string()melakukan hal yang sama denganString::from— hasilnya jugaString— hanya pintunya berupa metode pada literal. - Baris 4 (
let empty = String::new();):String::new()membuatStringtanpa isi sama sekali.emptysah dipakai dan akan tumbuh saat kita mulai menambahkan teks di Chapter selanjutnya. - Baris 5-7 (
println!…): ketiganya menunjukkan bahwaStringbisa dicetak seperti biasa, danlen()padaStringkosong melaporkan 0 byte.
Tiga Bidang di Stack dan Buffer di Heap #
Struktur data sebuah String terdiri atas dua bagian: kantor kecil di stack dan gudang besar di heap. Kantor di stack hanya memuat tiga field: penunjuk (pointer) menuju buffer di heap, jumlah byte yang terpakai (length), dan kapasitas buffer (capacity). Gudang di heap menyimpan byte UTF-8 sesungguhnya. Diagram berikut memetakan relasinya:
Pemisahan ini menjelaskan dua keajaiban yang sudah kita lihat di Section Ownership: menyalin field di stack itu murah (hanya penunjuk, panjang, kapasitas), tetapi karena dua String akan menunjuk buffer yang sama, Rust memilih meng-move kepemilikan alih-alih menyalin; dan ketika kita sungguh butuh dua salinan mandiri, clone() yang mahal itulah yang harus dibayar:
fn main() {
let report = String::from("Laporan Penjualan");
let archived = report.clone();
println!("report: {}", report);
println!("archived: {}", archived);
}Output yang Dihasilkan:
report: Laporan Penjualan
archived: Laporan PenjualanBedah kode di atas:
- Baris 2 (
let report = String::from("Laporan Penjualan");): buffer 17 byte dialokasikan di heap;reportmenjadi pemiliknya. - Baris 3 (
let archived = report.clone();):clone()mengalokasikan buffer heap kedua, menyalin seluruh 17 byte ke sana, danarchivedmenjadi pemilik buffer kedua. Dua pemilik, dua buffer — aturan ownership tetap aman. - Baris 4-5 (
println!…): kedua variabel sah dipakai karena masing-masing memiliki buffer sendiri. Tanpaclone,archived = reportakan memindahkan kepemilikan dan mematikanreport.
Teks yang Tumbuh dan Realokasi #
Pertumbuhan adalah alasan utama String eksis. Ketika teks baru ditambahkan melebihi kapasitas, Rust melakukan realokasi: mengalokasikan buffer yang lebih besar di heap, menyalin seluruh isi lama, membebaskan buffer lama, lalu memperbarui penunjuk serta kapasitas di stack. Proses penyalinan ini berbiaya linear terhadap panjang teks, dan strategi Rust adalah menggandakan kapasitas agar biaya total tetap murah rata-rata.
Kita bisa menyaksikan pertumbuhan itu lewat metode capacity():
fn main() {
let mut city = String::with_capacity(12);
println!("panjang: {}, kapasitas: {}", city.len(), city.capacity());
city.push_str("Yogyakarta");
println!("panjang: {}, kapasitas: {}", city.len(), city.capacity());
city.push_str(" Kota");
println!("panjang: {}, kapasitas: {}", city.len(), city.capacity());
}Output yang Dihasilkan:
panjang: 0, kapasitas: 12
panjang: 10, kapasitas: 12
panjang: 15, kapasitas: 24Mari kita telaah baris demi baris:
- Baris 2 (
let mut city = String::with_capacity(12);):String::with_capacity(12)mengalokasikan buffer 12 byte sejak awal — berguna ketika perkiraan panjang akhir sudah diketahui, supaya realokasi bisa dihindari. - Baris 3 (
println!("panjang: {}, kapasitas: {}", ...)): panjang 0 (belum ada isi) tetapi kapasitas sudah 12. Ruang tersedia belum berarti terpakai. - Baris 5 (
city.push_str("Yogyakarta");): sepuluh byte masuk ke buffer. Panjang naik menjadi 10, kapasitas tetap 12 karena masih cukup — tidak ada realokasi. - Baris 7 (
city.push_str(" Kota");): lima byte lagi dibutuhkan, padahal sisa kapasitas hanya 2. Rust merealokasi buffer menjadi kapasitas 24 (penggandaan dari 12), menyalin 10 byte lama, lalu menambahkan teks baru. Panjang kini 15.
&str atau String: Pedoman Memilih #
Dengan dua wujud teks di tangan, pedoman pemilihannya mengikuti aturan kepemilikan dari Section Ownership:
- Gunakan
&struntuk membaca: parameter fungsi pencari, penghitung, pembanding teks cukup meminjam — pemanggil boleh mengirim literal maupunString. - Gunakan
Stringketika fungsi atau variabel perlu memiliki teks yang hidup lebih lama, berubah, atau tumbuh — misalnya menyimpan input pengguna atau merangkai kalimat bertahap. - Gunakan
clone()hanya ketika dua salinan mandiri benar-benar diperlukan, karenaclonemenyalin seluruh buffer heap.
Ringkasan #
String adalah teks milik sendiri: tiga field kecil di stack (pointer, length, capacity) menjaga buffer byte UTF-8 di heap. Membuatnya mudah lewat String::from, .to_string(), String::new(), atau String::with_capacity. Saat teks melampaui kapasitas, Rust merealokasi dan menggandakan ruang — itulah harga pertumbuhan. Kepemilikan penuh berarti clone() diperlukan untuk salinan mandiri, sementara &str tetap jalan pinjam yang paling murah. Kapasitas dan kepemilikan ini menjadi panggung bagi pertanyaan berikutnya: bagaimana cara terbaik membangun dan mengubah isi String itu? Chapter selanjutnya menghadirkan push_str, insert, dan operator plus.