Rust String vs str
Rust has two string types that confuse many beginners: String and &str. Both hold text, but they work differently. Knowing when to use each one makes your code correct and efficient.
The Two String Types
String — Owned, Heap-Allocated
String is a growable text value stored on the heap. You own it, and you can add or remove characters from it.
let mut s = String::from("hello");
s.push_str(", world"); ← Append text
s.push('!'); ← Append a single character
println!("{}", s); ← hello, world!
&str — Borrowed String Slice
&str is a reference to a sequence of characters stored somewhere else — in the program binary or inside a String on the heap. You do not own it and cannot change it through this reference.
let literal: &str = "hello"; ← Points to text in the binary
The Billboard Diagram
String = A whiteboard you own. You can write, erase, and rewrite. &str = A photo of a billboard. You can read it but cannot change the original.
Where Each Type Lives
Type Storage Owned Mutable Grows/Shrinks ---- ------- ----- ------- ------------- String Heap Yes Yes Yes &str Anywhere No No No
String Literals Are &str
Every string literal you write in code is a &str. It points directly into the compiled binary and is valid for the entire program's lifetime:
let a = "hello"; ← Type: &str (string literal)
let b = String::from("hello"); ← Type: String (heap-allocated copy)
Converting Between the Two Types
String to &str
let owned = String::from("hello");
let borrowed: &str = &owned; ← Borrow a slice of the String
let also_borrowed = owned.as_str(); ← Explicit conversion
&str to String
let slice: &str = "hello"; let owned = slice.to_string(); ← Convert to String let also_owned = String::from(slice); ← Another way
The Photocopy Diagram
&str ──to_string()──→ String (make a copy you own) String ──&──────────→ &str (borrow a view of it)
Which Type to Use in Function Parameters
Functions that only read text should accept &str. This works with both String values and string literals, making the function more flexible:
fn print_greeting(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let owned = String::from("Alice");
print_greeting(&owned); ← Pass a &String → coerces to &str automatically
print_greeting("Bob"); ← Pass a literal &str directly
}
Functions that need to own the text, modify it, or store it should accept String.
Common String Operations
Concatenation
let s1 = String::from("hello");
let s2 = String::from(", world");
let s3 = s1 + &s2; ← s1 is moved here; s2 is borrowed
← s1 can no longer be used
println!("{}", s3); ← hello, world
The + operator moves the first String and appends the borrowed second. For joining many strings, the format! macro is cleaner:
let result = format!("{} {}", "hello", "world");
Length and Indexing
let s = String::from("hello");
println!("Length: {}", s.len()); ← 5 bytes
Rust measures string length in bytes, not characters. For text with non-ASCII characters, use .chars().count() instead.
Checking Content
let s = "hello world";
println!("{}", s.contains("world")); ← true
println!("{}", s.starts_with("hello")); ← true
println!("{}", s.ends_with("world")); ← true
Splitting and Trimming
let csv = "alice,bob,carol";
for name in csv.split(',') {
println!("{}", name);
}
let padded = " hello ";
println!("{}", padded.trim()); ← "hello"
String Indexing Warning
You cannot index a Rust string with s[0]. Strings are stored as UTF-8 bytes, and a single character can take 1 to 4 bytes. Indexing by byte position could split a character in half. Use slices only when you know the exact byte boundaries, or use .chars() to iterate character by character:
for c in "hello".chars() {
println!("{}", c);
}
Quick Reference
String::from("text") ← Create an owned String
"text" ← String literal (&str)
s.push_str("more") ← Append to a String
s.len() ← Length in bytes
s.contains("x") ← Check for substring
s.trim() ← Remove leading/trailing whitespace
s.split(',') ← Split into parts
&owned_string ← Borrow String as &str
slice.to_string() ← Convert &str to String
