Rust String Types Cheat Sheet
Quick-reference for String vs &str, conversions, slicing, and string methods. Each section includes copy-ready snippets with inline output comments.
String vs &str
Two main string types: String (owned, heap-allocated, growable) and &str (borrowed slice, immutable).
let mut s = String::from("hello");
s.push_str(" world");
println!("{s}"); // => "hello world"let literal: &str = "hello"; // string literal is &'static str
let owned = String::from("hello");
let slice: &str = &owned; // borrow as &str// &str for function parameters (accepts both types)
fn greet(name: &str) {
println!("Hello, {name}!");
}
greet("world"); // &str directly
greet(&String::from("world")); // &String coerces to &str
// String when you need ownership or mutation
fn make_greeting(name: &str) -> String {
format!("Hello, {name}!")
}Creating Strings
Multiple ways to create an owned String from various sources.
let s1 = String::from("hello");
let s2 = "hello".to_string();
let s3 = "hello".to_owned();All three are equivalent. String::from and .to_string() are most common.
let empty = String::new();
let mut buf = String::with_capacity(256); // pre-allocate
buf.push_str("hello");let bytes = vec![104, 101, 108, 108, 111];
let s = String::from_utf8(bytes).unwrap(); // => "hello"
// Lossy: replaces invalid bytes with U+FFFD
let s = String::from_utf8_lossy(&[104, 101, 255]);
// => "he�"let s: String = vec!['h', 'e', 'l', 'l', 'o'].into_iter().collect();
// => "hello"Conversions: String <-> &str
Convert between owned and borrowed strings. Deref coercion handles most &String to &str conversions automatically.
let s: String = String::from("hello");
let s: String = "hello".to_string();
let s: String = "hello".to_owned();let owned = String::from("hello");
let borrowed: &str = &owned; // deref coercion
let explicit: &str = owned.as_str(); // explicitfn takes_str(s: &str) { println!("{s}"); }
let owned = String::from("hello");
takes_str(&owned); // &String auto-coerces to &strBuilding Strings: push_str and push
Append to a String efficiently. push_str appends a &str; push appends a single char.
let mut s = String::from("hello");
s.push_str(" world");
println!("{s}"); // => "hello world"let mut s = String::from("hello");
s.push('!');
println!("{s}"); // => "hello!"let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = s1 + &s2; // s1 is moved, s2 is borrowed
// println!("{s1}"); // ERROR: s1 was moved
println!("{s3}"); // => "hello world"The + operator calls add(self, &str), consuming the left String. Avoid chaining + in loops.
format! for Concatenation
The format! macro builds a String without consuming any arguments. Most readable for complex concatenation.
let name = "Alice";
let age = 30;
let s = format!("{name} is {age} years old");
// => "Alice is 30 years old"let pi = 3.14159;
format!("{pi:.2}") // => "3.14"
format!("{:>10}", "hi") // => " hi"
format!("{:0>5}", 42) // => "00042"
format!("{:#b}", 255) // => "0b11111111"const GREETING: &str = concat!("hello", " ", "world");
// => "hello world" (no runtime allocation)String Slicing
Slicing works on byte positions. Panics if you slice inside a multi-byte character. Use get() for safe slicing.
let s = String::from("hello world");
let hello = &s[0..5]; // => "hello"
let world = &s[6..]; // => "world"let s = String::from("cafe\u{0301}"); // "cafe" + combining accent
// &s[0..5] // PANIC: byte 5 is inside the accent characterlet s = "cafe\u{0301}";
let safe = s.get(0..4); // => Some("cafe")
let bad = s.get(0..5); // => None (not a char boundary)
if let Some(slice) = s.get(0..4) {
println!("{slice}");
}Always use get() for ranges from user input or computation.
let s = "hello";
s.is_char_boundary(0) // => true
s.is_char_boundary(1) // => true
s.is_char_boundary(6) // => false (out of bounds)contains(), starts_with(), ends_with()
Boolean checks for substring presence. Also find() and rfind() for position lookup.
"hello world".contains("world") // => true
"hello world".contains("xyz") // => false"hello.rs".starts_with("hello") // => true
"hello.rs".ends_with(".rs") // => true"hello world hello".find("hello") // => Some(0)
"hello world hello".rfind("hello") // => Some(12)
"hello".find("xyz") // => None"hello world".find(' ') // => Some(5)
"hello world".find(|c: char| c.is_uppercase()) // => Nonesplit() and splitn()
Split strings into iterators of &str slices. Lazy evaluation -- no allocations until collected.
let parts: Vec<&str> = "a,b,c".split(',').collect();
// => ["a", "b", "c"]let words: Vec<&str> = "hello world".split_whitespace().collect();
// => ["hello", "world"] -- handles multiple spaceslet parts: Vec<&str> = "a:b:c:d".splitn(3, ':').collect();
// => ["a", "b", "c:d"]let parts: Vec<&str> = "a/b/c/d.txt".rsplitn(2, '/').collect();
// => ["d.txt", "a/b/c"]rsplitn yields elements in reverse order from the split point.
let lines: Vec<&str> = "line1\nline2\nline3".lines().collect();
// => ["line1", "line2", "line3"]chars() and bytes()
Iterate over Unicode characters with chars(). Iterate over raw bytes with bytes(). They yield different counts for non-ASCII text.
for c in "hello".chars() {
print!("{c} ");
}
// => h e l l olet s = "cafe\u{0301}"; // "cafe" + combining accent
s.len() // => 6 (bytes)
s.chars().count() // => 5 (Unicode scalar values).len() counts bytes, not characters. For user-perceived characters, use the unicode-segmentation crate.
let c = "hello".chars().nth(1); // => Some('e')
let c = "hello".chars().nth(99); // => Nonefor b in "hi".bytes() {
print!("{b} ");
}
// => 104 105trim and replace
Remove whitespace with trim. Substitute substrings with replace. Both return new values (strings are UTF-8 sequences).
" hello ".trim() // => "hello"
" hello ".trim_start() // => "hello "
" hello ".trim_end() // => " hello""##hello##".trim_matches('#') // => "hello""hello world".replace("world", "Rust") // => "hello Rust"
"aaa".replace('a', "b") // => "bbb""aaa".replacen('a', "b", 2) // => "bba""hello".to_uppercase() // => "HELLO"
"HELLO".to_lowercase() // => "hello"Capacity and Performance
Pre-allocate with with_capacity to avoid repeated reallocations in hot loops.
let mut s = String::with_capacity(100);
s.push_str("hello");
println!("len: {}, capacity: {}", s.len(), s.capacity());
// => len: 5, capacity: 100let lines = vec!["line1", "line2", "line3"];
let mut result = String::with_capacity(lines.iter().map(|s| s.len() + 1).sum());
for line in &lines {
result.push_str(line);
result.push('\n');
}Pre-computing capacity avoids reallocations. Each reallocation copies the entire buffer.
let mut s = String::with_capacity(1000);
s.push_str("short");
s.shrink_to_fit(); // capacity drops to ~5Can you write this from memory?
Create a String variable `name` with the value "Alice".