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 --> H1String | &str | |
|---|---|---|
| Ownership | Owned | Borrowed (reference) |
| Location data | Heap | Heap or data segment |
| Can be modified | Yes | No |
| Size in stack | 24 bytes (ptr + len + cap) | 16 bytes (ptr + len) |
| When to use | Need modification or ownership | Just 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('<', "<")
.replace('>', ">")
)
} 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 #
Stringvs&str—Stringis a modified owned heap-allocated;&stris a lightweight borrowed reference. Use&stras 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). Usechars()for characters orbytes()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 returnsResult, handle with?ormatch.with_capacityfor 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&strif there are no modifications,Stringif 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.