Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Cheat Sheets
  3. Rust
  4. Rust String Types Cheat Sheet
RustCheat Sheet

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.

On this page
  1. 1String vs &str
  2. 2Creating Strings
  3. 3Conversions: String <-> &str
  4. 4Building Strings: push_str and push
  5. 5format! for Concatenation
  6. 6String Slicing
  7. 7contains(), starts_with(), ends_with()
  8. 8split() and splitn()
  9. 9chars() and bytes()
  10. 10trim and replace
  11. 11Capacity and Performance
String vs &strCreating StringsConversions: String <-> &strBuilding Strings: push_str and pushformat! for ConcatenationString Slicingcontains(), starts_with(), ends_with()split() and splitn()chars() and bytes()trim and replaceCapacity and Performance

String vs &str

Two main string types: String (owned, heap-allocated, growable) and &str (borrowed slice, immutable).

String: owned, growable
let mut s = String::from("hello");
s.push_str(" world");
println!("{s}");  // => "hello world"
&str: borrowed slice
let literal: &str = "hello";     // string literal is &'static str
let owned = String::from("hello");
let slice: &str = &owned;         // borrow as &str
When to use which
// &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.

From a string literal
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.

Empty string and with capacity
let empty = String::new();
let mut buf = String::with_capacity(256);  // pre-allocate
buf.push_str("hello");
From bytes (UTF-8 validation)
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�"
From chars
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.

&str to String (3 equivalent ways)
let s: String = String::from("hello");
let s: String = "hello".to_string();
let s: String = "hello".to_owned();
String to &str
let owned = String::from("hello");
let borrowed: &str = &owned;           // deref coercion
let explicit: &str = owned.as_str();   // explicit
Deref coercion in function calls
fn takes_str(s: &str) { println!("{s}"); }

let owned = String::from("hello");
takes_str(&owned);  // &String auto-coerces to &str

Building Strings: push_str and push

Append to a String efficiently. push_str appends a &str; push appends a single char.

push_str: append a string slice
let mut s = String::from("hello");
s.push_str(" world");
println!("{s}");  // => "hello world"
push: append a single character
let mut s = String::from("hello");
s.push('!');
println!("{s}");  // => "hello!"
Concatenation with + (consumes the left operand)
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.

Basic format!
let name = "Alice";
let age = 30;
let s = format!("{name} is {age} years old");
// => "Alice is 30 years old"
Format specifiers
let pi = 3.14159;
format!("{pi:.2}")         // => "3.14"
format!("{:>10}", "hi")    // => "        hi"
format!("{:0>5}", 42)      // => "00042"
format!("{:#b}", 255)      // => "0b11111111"
Compile-time concatenation with concat!
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.

Basic slicing by byte position
let s = String::from("hello world");
let hello = &s[0..5];   // => "hello"
let world = &s[6..];    // => "world"
Panic on invalid byte boundary
let s = String::from("cafe\u{0301}");  // "cafe" + combining accent
// &s[0..5]  // PANIC: byte 5 is inside the accent character
Safe slicing with get()
let 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.

Check char boundary
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.

contains: substring check
"hello world".contains("world")   // => true
"hello world".contains("xyz")     // => false
starts_with and ends_with
"hello.rs".starts_with("hello")  // => true
"hello.rs".ends_with(".rs")      // => true
find and rfind: byte position of match
"hello world hello".find("hello")   // => Some(0)
"hello world hello".rfind("hello")  // => Some(12)
"hello".find("xyz")                 // => None
Search with char patterns
"hello world".find(' ')     // => Some(5)
"hello world".find(|c: char| c.is_uppercase())  // => None

split() and splitn()

Split strings into iterators of &str slices. Lazy evaluation -- no allocations until collected.

split on a delimiter
let parts: Vec<&str> = "a,b,c".split(',').collect();
// => ["a", "b", "c"]
split on whitespace
let words: Vec<&str> = "hello  world".split_whitespace().collect();
// => ["hello", "world"]  -- handles multiple spaces
splitn: limit the number of parts
let parts: Vec<&str> = "a:b:c:d".splitn(3, ':').collect();
// => ["a", "b", "c:d"]
rsplit: split from the right
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.

lines: split on line endings
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.

chars: iterate Unicode scalar values
for c in "hello".chars() {
    print!("{c} ");
}
// => h e l l o
chars().count() vs len()
let 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.

nth: get character by index
let c = "hello".chars().nth(1);  // => Some('e')
let c = "hello".chars().nth(99); // => None
bytes: raw byte iteration
for b in "hi".bytes() {
    print!("{b} ");
}
// => 104 105

trim and replace

Remove whitespace with trim. Substitute substrings with replace. Both return new values (strings are UTF-8 sequences).

trim, trim_start, trim_end
"  hello  ".trim()        // => "hello"
"  hello  ".trim_start()  // => "hello  "
"  hello  ".trim_end()    // => "  hello"
trim_matches: custom characters
"##hello##".trim_matches('#')  // => "hello"
replace: substitute all occurrences
"hello world".replace("world", "Rust")  // => "hello Rust"
"aaa".replace('a', "b")                 // => "bbb"
replacen: limit replacements
"aaa".replacen('a', "b", 2)  // => "bba"
to_uppercase and to_lowercase
"hello".to_uppercase()    // => "HELLO"
"HELLO".to_lowercase()   // => "hello"

Capacity and Performance

Pre-allocate with with_capacity to avoid repeated reallocations in hot loops.

Check capacity and length
let mut s = String::with_capacity(100);
s.push_str("hello");
println!("len: {}, capacity: {}", s.len(), s.capacity());
// => len: 5, capacity: 100
Efficient building in a loop
let 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.

shrink_to_fit: release excess capacity
let mut s = String::with_capacity(1000);
s.push_str("short");
s.shrink_to_fit();  // capacity drops to ~5
Learn Rust in Depth
Rust Ownership & Borrowing Practice →Rust Collections & Iterators Practice →
Warm-up1 / 2

Can you write this from memory?

Create a String variable `name` with the value "Alice".

See Also
Ownership & Borrowing →Lifetimes →

Start Practicing Rust

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.