Strings

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 --> H1
String&str
OwnershipOwnedBorrowed (referensi)
Lokasi dataHeapHeap atau data segment
Bisa dimodifikasiYaTidak
Ukuran di stack24 byte (ptr + len + cap)16 byte (ptr + len)
Kapan digunakanButuh modifikasi atau ownershipBaca 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('<', "&lt;")
                .replace('>', "&gt;")
        )
    } 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 #

  • String vs &strString adalah owned heap-allocated yang bisa dimodifikasi; &str adalah borrowed reference yang ringan. Gunakan &str sebagai 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). Gunakan chars() untuk karakter atau bytes() 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 mengembalikan Result, tangani dengan ? atau match.
  • with_capacity untuk 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 &str jika tidak ada modifikasi, String jika 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.

← Sebelumnya: Artikel & Sumber Daya   Berikutnya: IO →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact