Strings #

Strings in Rust are one of the most confusing topics for beginners — especially because Rust has two different main string types: String (owned, heap-allocated, can be modified) and &str (borrowed reference, can be to the heap or data segment, cannot be modified). This isn’t a design quirk — it’s a direct consequence of Rust’s proprietary system that provides complete control over memory allocation. Once you understand the difference between the two, all other string operations will feel natural. This article covers all the standard string operations available in std::string::String and str — without external dependencies.

String vs &str — Fundamental Differences #

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 (reference)
Location dataHeapHeap or data segment
Can be modifiedYesNo
Size in stack24 bytes (ptr + len + cap)16 bytes (ptr + len)
When to useNeed modification or ownershipJust read, function parameters
fn main() {
    // &str — string literal, stored in binary segment data
    let literal: &str = "Halo, dunia!";

    // String — owned, stored on the heap
    let owned: String = String::from("Halo, dunia!");
    let owned2: String = "Halo".to_string();
    let owned3: String = "Halo".to_owned();

    // &str from String (borrowing)
    let slice: &str = &owned;          // borrows the entire string
    let bagian: &str = &owned[0..4];   // partial borrow ("Hello")

    // String of &str
    let kembali: String = slice.to_string();
    let kembali2: String = String::from(literal);

    println!("{} {} {}", literal, owned, bagian);
}

Use &str for Function Parameters #

// ANTI-PATTERN: receiving a String forces the caller to create an owned String
fn cetak_buruk(s: String) {
    println!("{}", s);
}

// TRUE: &str accepts both — String and &str
fn cetak_baik(s: &str) {
    println!("{}", s);
}

fn main() {
    let owned = String::from("hello");
    let literal = "world";

    // bad_print can only accept Strings
    cetak_buruk(owned.clone());          // must be cloned!
    // bad_print(literal);            // error: type mismatch

    // fine_print accepts both
    cetak_baik(&owned);                  // ✓ deref coercion
    cetak_baik(literal);                 // ✓
}

Creating and Concatenating Strings #

fn main() {
    // Creates an empty String
    let mut s = String::new();

    // push_str — add &str to String
    s.push_str("Halo");
    s.push_str(", dunia");

    // push — add one character
    s.push('!');
    println!("{}", s);  // "Hello, world!"

    // Operator + (consumes left String)
    let s1 = String::from("Halo, ");
    let s2 = String::from("dunia!");
    let s3 = s1 + &s2;  // s1 moved, s2 borrowed
    // println!("{}", s1);  // error: s1 has been moved
    println!("{}", s3);

    // format! — the most flexible way, does not consume anything
    let s1 = String::from("Halo");
    let s2 = String::from("dunia");
    let s3 = format!("{}, {}!", s1, s2);
    println!("{} {} {}", s1, s2, s3);  // are all still valid

    // Combine Vec<String> or Vec<&str>
    let kata = vec!["satu", "dua", "tiga", "empat"];
    let digabung = kata.join(", ");
    println!("{}", digabung);  // "one, two, three, four"

    let dengan_newline = kata.join("\n");
    println!("{}", dengan_newline);

    // concat — no separator
    let tanpa_sep = ["a", "b", "c"].concat();
    println!("{}", tanpa_sep);  // "abc"

    // repeat
    let diulang = "ha".repeat(3);
    println!("{}", diulang);  // "hahaha"

    // with_capacity — initial capacity allocation to avoid reallocation
    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());
}

Search and Check #

fn main() {
    let teks = "Halo, dunia! Ini adalah Rust.";

    // contains — checks for the existence of a 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 — first position found (byte index)
    println!("{:?}", teks.find("dunia"));     // Some(6)
    println!("{:?}", teks.find("xyz"));       // None

    // rfind — search from right
    let teks2 = "kucing kecil kucing besar";
    println!("{:?}", teks2.rfind("kucing")); // Some(13)

    // matches — count occurrences
    let jumlah = teks2.matches("kucing").count();
    println!("Muncul {} kali", jumlah);      // 2

    // String length
    let unicode = "Halo 🌏";
    println!("len (byte): {}", unicode.len());           // 10 (🌏 = 4 bytes)
    println!("chars: {}", unicode.chars().count());      // 6 (6 characters)
    println!("kosong: {}", "".is_empty());

    // is_ascii — are all ASCII characters
    println!("{}", "hello".is_ascii());  // true
    println!("{}", "héllo".is_ascii());  // false
}

Transformation and Manipulation #

fn main() {
    let teks = "  Halo, Dunia!  ";

    // Trim — remove whitespace at the ends
    println!("'{}'", teks.trim());         // 'Hello, World!'
    println!("'{}'", teks.trim_start());   // 'Hello, World!  '
    println!("'{}'", teks.trim_end());     // ' Hello, World!'

    // Specific character trim
    let dengan_strip = "###Halo###";
    println!("{}", dengan_strip.trim_matches('#'));  // "Hello"
    println!("{}", dengan_strip.trim_start_matches('#'));  // "Hello###"

    // Case conversion
    let campur = "Halo Dunia RUST";
    println!("{}", campur.to_lowercase());   // "hello rust world"
    println!("{}", campur.to_uppercase());   // "HELLO RUST WORLD"

    // Replace — replace all occurrences
    let kalimat = "kucing suka ikan, kucing suka tidur";
    println!("{}", kalimat.replace("kucing", "anjing"));
    // "dogs like fish, dogs like to sleep"

    // replacen — replace the first N occurrences
    println!("{}", kalimat.replacen("kucing", "anjing", 1));
    // "dogs like fish, cats like to sleep"

    // replace with closure (not present in std, use regex for this)

    // strip_prefix / strip_suffix — remove prefix/suffix if present
    let url = "https://example.com";
    if let Some(domain) = url.strip_prefix("https://") {
        println!("Domain: {}", domain);  // "example.com"
    }
    let file = "laporan.pdf";
    if let Some(nama) = file.strip_suffix(".pdf") {
        println!("Nama: {}", nama);  // "report"
    }

    // to_ascii_uppercase / lowercase — for ASCII characters only
    println!("{}", "hello".to_ascii_uppercase());
}

Split and Parse #

fn main() {
    let csv = "satu,dua,tiga,empat,lima";

    // split — iterator of substrings
    let bagian: Vec<&str> = csv.split(',').collect();
    println!("{:?}", bagian);  // ["one", "two", "three", "four", "five"]

    // split with strings
    let teks = "kata1::kata2::kata3";
    let kata: Vec<&str> = teks.split("::").collect();
    println!("{:?}", kata);

    // splitn — limit the number of parts
    let terbatas: Vec<&str> = csv.splitn(3, ',').collect();
    println!("{:?}", terbatas);  // ["one", "two", "three, four, five"]

    // split_whitespace — split based on all whitespace
    let banyak_spasi = "  satu   dua\ttiga\nempat  ";
    let kata: Vec<&str> = banyak_spasi.split_whitespace().collect();
    println!("{:?}", kata);  // ["one", "two", "three", "four"]

    // lines — split by line
    let multi = "baris 1\nbaris 2\nbaris 3";
    for baris in multi.lines() {
        println!("  → {}", baris);
    }

    // Parsing to another type
    let angka: i32 = "42".parse().unwrap();
    let pi: f64 = "3.14".parse().unwrap();
    println!("{} {}", angka, pi);

    // parse with error handling
    match "bukan-angka".parse::<i32>() {
        Ok(n) => println!("Angka: {}", n),
        Err(e) => println!("Gagal parse: {}", e),
    }

    // chars().enumerate() for iteration with index
    for (i, c) in "Halo".chars().enumerate() {
        println!("  [{}] = '{}'", i, c);
    }
}

Character and Byte Iteration #

Rust strings are UTF-8 — it’s important to understand the difference between byte iteration and Unicode character iteration:

fn main() {
    let teks = "Halo 🌏";

    // chars() — iteration of Unicode scalar value (char)
    println!("Karakter:");
    for c in teks.chars() {
        print!("  '{}' ", c);
    }
    println!();

    // bytes() — raw byte iteration (u8)
    println!("\nByte ({} byte total):", teks.len());
    for b in teks.bytes() {
        print!("{:02x} ", b);
    }
    println!();

    // Be careful slicing strings — must be within the UTF-8 character limit
    // ANTI-PATTERN: can panic if it cuts off in the middle of a multibyte character
    // let slice = &text[0..5];  // panic! if 5 cuts 🌏

    // TRUE: use char_indices to get valid limits
    let batas: Vec<(usize, char)> = teks.char_indices().collect();
    println!("\nChar indices: {:?}", batas);

    // Safely retrieve the first N characters
    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));

    // Collect chars to String
    let hanya_ascii: String = teks.chars()
        .filter(|c| c.is_ascii())
        .collect();
    println!("Hanya ASCII: '{}'", hanya_ascii);  // "Hello"

    // Transformations per character
    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! — the most common way to create a String from a value
    let s = format!("Nama: {}, Usia: {}", "Budi", 28);

    // Padding and alignment
    println!("{:>10}", "kanan");     // "right" (right aligned, width 10)
    println!("{:<10}", "kiri");      // "left" (aligned left)
    println!("{:^10}", "tengah");    // "center" (center aligned)
    println!("{:*>10}", "Halo");     // "******Hello" (padding with *)

    // Number
    println!("{:05}", 42);           // "00042" (zero-padding)
    println!("{:+}", 42);            // "+42" (force sign)
    println!("{:.2}", 3.14159);      // "3.14" (2 decimal)
    println!("{:8.2}", 3.14159);     // " 3.14" (width 8, 2 decimals)
    println!("{:e}", 1_000_000.0);   // "1e6" (scientific notation)

    // Number base
    println!("{:b}", 42);            // "101010" (binary)
    println!("{:o}", 42);            // "52" (octal)
    println!("{:x}", 255);           // "ff" (small hex)
    println!("{:X}", 255);           // "FF" (big hex)
    println!("{:#x}", 255);          // "0xff" (with 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> allows the function to return &str if there are no modifications, or String if there are — without unnecessary allocations:

use std::borrow::Cow;

// Return &str if no characters were replaced,
// String if present — no allocation unless necessary
fn sanitasi(input: &str) -> Cow<str> {
    if input.contains('<') || input.contains('>') {
        // Something needs to be replaced — new String allocation
        Cow::Owned(
            input
                .replace('<', "&lt;")
                .replace('>', "&gt;")
        )
    } else {
        // Nothing to replace — return reference
        Cow::Borrowed(input)
    }
}

fn main() {
    let aman = sanitasi("Halo dunia");
    let tidak_aman = sanitasi("<script>alert('xss')</script>");

    println!("{}", aman);         // no new allocation
    println!("{}", tidak_aman);   // New string allocated

    // Cow can be used as &str
    let panjang = aman.len();

    // Convert to String if you need ownership
    let owned: String = aman.into_owned();
}

General Type Conversion #

fn main() {
    // Number to String
    let n: i32 = 42;
    let s = n.to_string();
    let s2 = format!("{}", n);

    // Float to String with precision
    let f: f64 = 3.14159;
    let s_float = format!("{:.2}", f);  // "3.14"

    // Bool to String
    let b = true;
    println!("{}", b.to_string());  // "true"

    // String to bytes and back
    let s = String::from("Halo");
    let bytes: Vec<u8> = s.into_bytes();
    let kembali = String::from_utf8(bytes).unwrap();

    // &str to bytes
    let b: &[u8] = "Halo".as_bytes();

    // Bytes to &str (can fail if not valid UTF-8)
    match std::str::from_utf8(b) {
        Ok(s) => println!("Valid UTF-8: {}", s),
        Err(e) => println!("Bukan UTF-8: {}", e),
    }

    // Lossy conversion — replace invalid byte with replacement char
    let lossy = String::from_utf8_lossy(b);
    println!("{}", lossy);

    // OsString (for paths and file names in the operating system)
    let os_str = std::ffi::OsString::from("path/file.txt");
    if let Some(s) = os_str.to_str() {
        println!("OsString ke &str: {}", s);
    }
}

Summary #

  • String vs &strString is a modified owned heap-allocated; &str is a lightweight borrowed reference. Use &str as a function parameter to accept both.
  • format! to combine — safer than + operator because it does not consume any ownership. Use + only for simple cases.
  • Rust string is UTF-8 — cannot be indexed directly (s[0] is an error). Use chars() for characters or bytes() for raw bytes.
  • split_whitespace() for simple tokenization — handles spaces, tabs, and newlines all at once, and ignores repeated whitespace.
  • trim() returns &str — doesn’t create a new String, just shifts the pointer and shortens the length. Very efficient.
  • parse::<T>() for String conversion to another type — always returns Result, handle with ? or match.
  • with_capacity for Incrementally constructed String — if knows the estimated final size, allocates initial capacity to avoid repeated reallocations.
  • Cow<str> for functions that may or may not require allocation — returns &str if there are no modifications, String if there are, without forcing unnecessary allocations.
  • char_indices() for safe slicing — always use this to get a valid position when needing to cut the string at a specific character position.


← Previous: Articles & Resources   Next: IO →

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