Strings #
String di Rust adalah salah satu topik yang paling membingungkan bagi pemula — terutama karena Rust punya dua tipe string utama yang berbeda: String (owned, heap-allocated, bisa dimodifikasi) dan &str (borrowed reference, bisa ke heap atau data segment, tidak bisa dimodifikasi). Ini bukan keanehan desain — ini konsekuensi langsung dari sistem ownership Rust yang memberikan kontrol penuh atas alokasi memori. Setelah memahami perbedaan keduanya, semua operasi string lainnya akan terasa natural. Artikel ini membahas semua operasi string standar yang tersedia di std::string::String dan str — tanpa dependensi eksternal.
String vs &str — Perbedaan Fundamental
#
flowchart LR
subgraph Stack
SR["&str\n(fat pointer)\nptr + len"]
S["String\nptr + len + capacity"]
end
subgraph Heap
H1["'Halo, dunia!'\n(heap-allocated)"]
end
subgraph "Data Segment"
DS["'literal'\n(compile-time constant)"]
end
SR -->|bisa merujuk ke| DS
SR -->|bisa merujuk ke| H1
S --> H1String | &str | |
|---|---|---|
| Ownership | Owned | Borrowed (referensi) |
| Lokasi data | Heap | Heap atau data segment |
| Bisa dimodifikasi | Ya | Tidak |
| Ukuran di stack | 24 byte (ptr + len + cap) | 16 byte (ptr + len) |
| Kapan digunakan | Butuh modifikasi atau ownership | Baca saja, parameter fungsi |
fn main() {
// &str — string literal, disimpan di data segment binary
let literal: &str = "Halo, dunia!";
// String — owned, disimpan di heap
let owned: String = String::from("Halo, dunia!");
let owned2: String = "Halo".to_string();
let owned3: String = "Halo".to_owned();
// &str dari String (borrowing)
let slice: &str = &owned; // borrow seluruh string
let bagian: &str = &owned[0..4]; // borrow sebagian ("Halo")
// String dari &str
let kembali: String = slice.to_string();
let kembali2: String = String::from(literal);
println!("{} {} {}", literal, owned, bagian);
}
Gunakan &str untuk Parameter Fungsi
#
// ANTI-PATTERN: menerima String memaksa caller membuat owned String
fn cetak_buruk(s: String) {
println!("{}", s);
}
// BENAR: &str menerima keduanya — String dan &str
fn cetak_baik(s: &str) {
println!("{}", s);
}
fn main() {
let owned = String::from("hello");
let literal = "world";
// cetak_buruk hanya bisa menerima String
cetak_buruk(owned.clone()); // harus clone!
// cetak_buruk(literal); // error: tipe tidak cocok
// cetak_baik menerima keduanya
cetak_baik(&owned); // ✓ deref coercion
cetak_baik(literal); // ✓
}
Membuat dan Menggabungkan String #
fn main() {
// Membuat String kosong
let mut s = String::new();
// push_str — tambahkan &str ke String
s.push_str("Halo");
s.push_str(", dunia");
// push — tambahkan satu karakter
s.push('!');
println!("{}", s); // "Halo, dunia!"
// Operator + (mengkonsumsi String kiri)
let s1 = String::from("Halo, ");
let s2 = String::from("dunia!");
let s3 = s1 + &s2; // s1 di-move, s2 di-borrow
// println!("{}", s1); // error: s1 sudah di-move
println!("{}", s3);
// format! — cara paling fleksibel, tidak mengkonsumsi apapun
let s1 = String::from("Halo");
let s2 = String::from("dunia");
let s3 = format!("{}, {}!", s1, s2);
println!("{} {} {}", s1, s2, s3); // semua masih valid
// Gabungkan Vec<String> atau Vec<&str>
let kata = vec!["satu", "dua", "tiga", "empat"];
let digabung = kata.join(", ");
println!("{}", digabung); // "satu, dua, tiga, empat"
let dengan_newline = kata.join("\n");
println!("{}", dengan_newline);
// concat — tanpa separator
let tanpa_sep = ["a", "b", "c"].concat();
println!("{}", tanpa_sep); // "abc"
// repeat
let diulang = "ha".repeat(3);
println!("{}", diulang); // "hahaha"
// with_capacity — alokasi kapasitas awal untuk menghindari realokasi
let mut s = String::with_capacity(50);
for kata in &["satu", "dua", "tiga"] {
s.push_str(kata);
s.push(' ');
}
println!("'{}' (cap: {})", s.trim(), s.capacity());
}
Pencarian dan Pemeriksaan #
fn main() {
let teks = "Halo, dunia! Ini adalah Rust.";
// contains — cek keberadaan substring
println!("{}", teks.contains("Rust")); // true
println!("{}", teks.contains("Python")); // false
// starts_with / ends_with
println!("{}", teks.starts_with("Halo")); // true
println!("{}", teks.ends_with("Rust.")); // true
// find — posisi pertama ditemukan (indeks byte)
println!("{:?}", teks.find("dunia")); // Some(6)
println!("{:?}", teks.find("xyz")); // None
// rfind — cari dari kanan
let teks2 = "kucing kecil kucing besar";
println!("{:?}", teks2.rfind("kucing")); // Some(13)
// matches — hitung kemunculan
let jumlah = teks2.matches("kucing").count();
println!("Muncul {} kali", jumlah); // 2
// Panjang string
let unicode = "Halo 🌏";
println!("len (byte): {}", unicode.len()); // 10 (🌏 = 4 byte)
println!("chars: {}", unicode.chars().count()); // 6 (6 karakter)
println!("kosong: {}", "".is_empty());
// is_ascii — apakah semua karakter ASCII
println!("{}", "hello".is_ascii()); // true
println!("{}", "héllo".is_ascii()); // false
}
Transformasi dan Manipulasi #
fn main() {
let teks = " Halo, Dunia! ";
// Trim — hapus whitespace di ujung
println!("'{}'", teks.trim()); // 'Halo, Dunia!'
println!("'{}'", teks.trim_start()); // 'Halo, Dunia! '
println!("'{}'", teks.trim_end()); // ' Halo, Dunia!'
// Trim karakter spesifik
let dengan_strip = "###Halo###";
println!("{}", dengan_strip.trim_matches('#')); // "Halo"
println!("{}", dengan_strip.trim_start_matches('#')); // "Halo###"
// Konversi case
let campur = "Halo Dunia RUST";
println!("{}", campur.to_lowercase()); // "halo dunia rust"
println!("{}", campur.to_uppercase()); // "HALO DUNIA RUST"
// Replace — ganti semua kemunculan
let kalimat = "kucing suka ikan, kucing suka tidur";
println!("{}", kalimat.replace("kucing", "anjing"));
// "anjing suka ikan, anjing suka tidur"
// replacen — ganti N kemunculan pertama
println!("{}", kalimat.replacen("kucing", "anjing", 1));
// "anjing suka ikan, kucing suka tidur"
// replace dengan closure (tidak ada di std, gunakan regex untuk ini)
// strip_prefix / strip_suffix — hapus prefix/suffix jika ada
let url = "https://contoh.com";
if let Some(domain) = url.strip_prefix("https://") {
println!("Domain: {}", domain); // "contoh.com"
}
let file = "laporan.pdf";
if let Some(nama) = file.strip_suffix(".pdf") {
println!("Nama: {}", nama); // "laporan"
}
// to_ascii_uppercase / lowercase — hanya untuk karakter ASCII
println!("{}", "hello".to_ascii_uppercase());
}
Split dan Parsing #
fn main() {
let csv = "satu,dua,tiga,empat,lima";
// split — iterator dari substring
let bagian: Vec<&str> = csv.split(',').collect();
println!("{:?}", bagian); // ["satu", "dua", "tiga", "empat", "lima"]
// split dengan string
let teks = "kata1::kata2::kata3";
let kata: Vec<&str> = teks.split("::").collect();
println!("{:?}", kata);
// splitn — batasi jumlah bagian
let terbatas: Vec<&str> = csv.splitn(3, ',').collect();
println!("{:?}", terbatas); // ["satu", "dua", "tiga,empat,lima"]
// split_whitespace — pisah berdasarkan semua whitespace
let banyak_spasi = " satu dua\ttiga\nempat ";
let kata: Vec<&str> = banyak_spasi.split_whitespace().collect();
println!("{:?}", kata); // ["satu", "dua", "tiga", "empat"]
// lines — pisah per baris
let multi = "baris 1\nbaris 2\nbaris 3";
for baris in multi.lines() {
println!(" → {}", baris);
}
// Parsing ke tipe lain
let angka: i32 = "42".parse().unwrap();
let pi: f64 = "3.14".parse().unwrap();
println!("{} {}", angka, pi);
// parse dengan error handling
match "bukan-angka".parse::<i32>() {
Ok(n) => println!("Angka: {}", n),
Err(e) => println!("Gagal parse: {}", e),
}
// chars().enumerate() untuk iterasi dengan indeks
for (i, c) in "Halo".chars().enumerate() {
println!(" [{}] = '{}'", i, c);
}
}
Iterasi Karakter dan Byte #
String Rust adalah UTF-8 — penting untuk memahami perbedaan antara iterasi byte dan iterasi karakter Unicode:
fn main() {
let teks = "Halo 🌏";
// chars() — iterasi Unicode scalar value (char)
println!("Karakter:");
for c in teks.chars() {
print!(" '{}' ", c);
}
println!();
// bytes() — iterasi byte mentah (u8)
println!("\nByte ({} byte total):", teks.len());
for b in teks.bytes() {
print!("{:02x} ", b);
}
println!();
// Hati-hati slicing string — harus di batas karakter UTF-8
// ANTI-PATTERN: bisa panic jika memotong di tengah karakter multibyte
// let slice = &teks[0..5]; // panic! jika 5 memotong 🌏
// BENAR: gunakan char_indices untuk mendapat batas yang valid
let batas: Vec<(usize, char)> = teks.char_indices().collect();
println!("\nChar indices: {:?}", batas);
// Ambil N karakter pertama dengan aman
fn ambil_n_char(s: &str, n: usize) -> &str {
match s.char_indices().nth(n) {
Some((idx, _)) => &s[..idx],
None => s,
}
}
println!("5 char pertama: '{}'", ambil_n_char(teks, 5));
// Kumpulkan chars ke String
let hanya_ascii: String = teks.chars()
.filter(|c| c.is_ascii())
.collect();
println!("Hanya ASCII: '{}'", hanya_ascii); // "Halo "
// Transformasi per karakter
let title_case: String = teks
.chars()
.enumerate()
.map(|(i, c)| if i == 0 { c.to_uppercase().next().unwrap() } else { c })
.collect();
}
Format String #
fn main() {
// format! — cara paling umum membuat String dari nilai
let s = format!("Nama: {}, Usia: {}", "Budi", 28);
// Padding dan alignment
println!("{:>10}", "kanan"); // " kanan" (rata kanan, lebar 10)
println!("{:<10}", "kiri"); // "kiri " (rata kiri)
println!("{:^10}", "tengah"); // " tengah " (rata tengah)
println!("{:*>10}", "Halo"); // "******Halo" (padding dengan *)
// Angka
println!("{:05}", 42); // "00042" (zero-padding)
println!("{:+}", 42); // "+42" (paksa tanda)
println!("{:.2}", 3.14159); // "3.14" (2 desimal)
println!("{:8.2}", 3.14159); // " 3.14" (lebar 8, 2 desimal)
println!("{:e}", 1_000_000.0); // "1e6" (scientific notation)
// Basis bilangan
println!("{:b}", 42); // "101010" (biner)
println!("{:o}", 42); // "52" (oktal)
println!("{:x}", 255); // "ff" (hex kecil)
println!("{:X}", 255); // "FF" (hex besar)
println!("{:#x}", 255); // "0xff" (dengan prefix)
println!("{:#010x}", 255); // "0x000000ff"
// Debug vs Display
let vec = vec![1, 2, 3];
println!("{:?}", vec); // [1, 2, 3]
println!("{:#?}", vec); // pretty-printed
// Named argument
let nama = "Budi";
let usia = 28;
let s = format!("{nama} berusia {usia} tahun");
println!("{}", s);
}
Cow<str> — Clone on Write
#
Cow<'a, str> memungkinkan fungsi mengembalikan &str jika tidak ada modifikasi, atau String jika ada — tanpa alokasi yang tidak perlu:
use std::borrow::Cow;
// Kembalikan &str jika tidak ada karakter yang diganti,
// String jika ada — tanpa alokasi kecuali diperlukan
fn sanitasi(input: &str) -> Cow<str> {
if input.contains('<') || input.contains('>') {
// Ada yang perlu diganti — alokasi String baru
Cow::Owned(
input
.replace('<', "<")
.replace('>', ">")
)
} else {
// Tidak ada yang perlu diganti — kembalikan referensi
Cow::Borrowed(input)
}
}
fn main() {
let aman = sanitasi("Halo dunia");
let tidak_aman = sanitasi("<script>alert('xss')</script>");
println!("{}", aman); // tidak ada alokasi baru
println!("{}", tidak_aman); // String baru dialokasikan
// Cow bisa digunakan sebagai &str
let panjang = aman.len();
// Konversi ke String jika butuh ownership
let owned: String = aman.into_owned();
}
Konversi Tipe Umum #
fn main() {
// Angka ke String
let n: i32 = 42;
let s = n.to_string();
let s2 = format!("{}", n);
// Float ke String dengan presisi
let f: f64 = 3.14159;
let s_float = format!("{:.2}", f); // "3.14"
// Bool ke String
let b = true;
println!("{}", b.to_string()); // "true"
// String ke bytes dan kembali
let s = String::from("Halo");
let bytes: Vec<u8> = s.into_bytes();
let kembali = String::from_utf8(bytes).unwrap();
// &str ke bytes
let b: &[u8] = "Halo".as_bytes();
// Bytes ke &str (bisa gagal jika bukan UTF-8 valid)
match std::str::from_utf8(b) {
Ok(s) => println!("Valid UTF-8: {}", s),
Err(e) => println!("Bukan UTF-8: {}", e),
}
// Konversi lossy — ganti byte tidak valid dengan replacement char
let lossy = String::from_utf8_lossy(b);
println!("{}", lossy);
// OsString (untuk path dan nama file di sistem operasi)
let os_str = std::ffi::OsString::from("path/file.txt");
if let Some(s) = os_str.to_str() {
println!("OsString ke &str: {}", s);
}
}
Ringkasan #
Stringvs&str—Stringadalah owned heap-allocated yang bisa dimodifikasi;&stradalah borrowed reference yang ringan. Gunakan&strsebagai parameter fungsi agar bisa menerima keduanya.format!untuk menggabungkan — lebih aman dari operator+karena tidak mengkonsumsi ownership apapun. Gunakan+hanya untuk kasus sederhana.- String Rust adalah UTF-8 — tidak bisa diindeks langsung (
s[0]adalah error). Gunakanchars()untuk karakter ataubytes()untuk byte mentah.split_whitespace()untuk tokenisasi sederhana — menangani spasi, tab, dan newline sekaligus, dan mengabaikan whitespace berulang.trim()mengembalikan&str— tidak membuat String baru, hanya menggeser pointer dan memperpendek length. Sangat efisien.parse::<T>()untuk konversi String ke tipe lain — selalu mengembalikanResult, tangani dengan?ataumatch.with_capacityuntuk String yang dibangun inkremental — jika tahu perkiraan ukuran akhir, alokasikan kapasitas awal untuk menghindari realokasi berulang.Cow<str>untuk fungsi yang mungkin perlu atau tidak perlu alokasi — kembalikan&strjika tidak ada modifikasi,Stringjika ada, tanpa memaksa alokasi yang tidak perlu.char_indices()untuk slicing yang aman — selalu gunakan ini untuk mendapat posisi yang valid saat perlu memotong string di posisi karakter tertentu.