TL;DR: Rust has two primary string types.
Stringis an owned, heap-allocated, growable UTF-8 string: like aVec<u8>that is guaranteed to be valid UTF-8.&stris a borrowed string slice: a reference to a sequence of UTF-8 bytes stored somewhere (stack, heap, or the binary). UseStringwhen you need to own or mutate a string; use&strin function parameters when you only need to read. Both are always valid UTF-8; Rust has no undefined encoding behavior.
What Is the Difference Between String and &str?
String owns its data on the heap and can grow. &str is a read-only view into string data owned by someone else; it is always a borrowed reference.
fn main() {
// &str: string literal, stored in the program binary, 'static lifetime
let s1: &str = "hello world";
// String: owned, heap-allocated, growable
let s2: String = String::from("hello world");
let s3: String = "hello world".to_string();
let s4: String = format!("hello {}", "world");
// &str from String: borrow the String's contents
let s5: &str = &s2;
let s6: &str = &s2[0..5]; // "hello": a slice
// String is mutable; &str is always read-only
let mut owned = String::from("hello");
owned.push_str(", world!");
println!("{owned}"); // "hello, world!"
}Which One Should I Use in Function Parameters?
Use &str for function parameters: it accepts both String references and &str literals without the caller needing to own a String.
// Good; accepts any string-like argument
fn greet(name: &str) {
println!("Hello, {name}!");
}
// Less flexible; caller must provide an owned String
fn greet_owned(name: String) {
println!("Hello, {name}!");
}
fn main() {
greet("Alice"); // &str literal; works
greet(&String::from("Bob")); // &String coerces to &str; works
greet_owned(String::from("Charlie")); // works
// greet_owned("Dave"); // ERROR: expected String, got &str
}The rule of thumb: accept &str, return String when writing functions. Use String in structs when the struct needs to own its data. Use &str in structs only when the struct borrows from data that outlives it (requires lifetime annotations).
How Do You Convert Between String and &str?
&str → String via .to_string() or String::from(). String → &str via & (deref coercion) or as_str().
fn main() {
// &str → String (allocates)
let owned: String = "hello".to_string();
let owned2: String = String::from("hello");
let owned3: String = "hello".to_owned();
// String → &str (zero-cost borrow)
let borrowed: &str = &owned;
let borrowed2: &str = owned.as_str();
let slice: &str = &owned[1..4]; // "ell"
// Concatenation: + operator consumes the left String
let s1 = String::from("hello, ");
let s2 = String::from("world!");
let combined = s1 + &s2; // s1 is moved here; s2 is borrowed
// s1 is no longer valid; s2 still is
// format!: does not consume anything
let s1 = String::from("hello, ");
let combined2 = format!("{s1}{s2}");
}Why Does Rust Have Two String Types?
The split between owned (String) and borrowed (&str) is a direct consequence of Rust's ownership model; it allows Rust to avoid unnecessary heap allocations.
In languages with garbage collection (Python, Java, Go), strings are always heap objects; every string literal creates an allocation. In Rust:
- String literals (
"hello") are embedded in the compiled binary and have'staticlifetime: no allocation. &strcan reference binary data, stack data, or heap data: no copy, no allocation.Stringis allocated only when you need ownership or mutation.
This means parsing a large file and extracting substrings can be done entirely with &str slices: zero copies while a GC language would allocate a new string for each substring.
How Does Rust Handle Non-ASCII and Unicode?
Rust strings are always valid UTF-8. Indexing by byte position is supported; indexing by character is not (because characters have variable byte width).
fn main() {
let s = "héllo"; // UTF-8: 'é' is 2 bytes
println!("{}", s.len()); // 6 (bytes, not chars)
println!("{}", s.chars().count()); // 5 (Unicode scalar values)
// Iterate over characters safely
for c in s.chars() {
print!("{c} "); // h é l l o
}
// Byte slicing: must be on valid UTF-8 boundaries
let hello = &s[0..1]; // "h": safe, 1-byte char
// let bad = &s[0..2]; // PANIC at runtime: 'é' is 2 bytes, cut in half
// Safe char-based slicing
let first_two: String = s.chars().take(2).collect(); // "hé"
}To work with individual bytes: s.as_bytes() returns &[u8]. To index by grapheme clusters (user-perceived characters), use the unicode-segmentation crate.
Frequently Asked Questions
&String is a reference to an owned String. &str is a string slice. Rust's deref coercion automatically converts &String to &str in most contexts; functions accepting &str work transparently with &String arguments. Prefer &str in function signatures for maximum flexibility.
str is an unsized type: a dynamically-sized sequence of UTF-8 bytes. It cannot exist on its own because the compiler doesn't know its size. You always use it behind a reference (&str) or a box (Box<str>). In practice, you almost never write str directly.
Box<str> is a heap-allocated, immutable string slice: slightly smaller than String (no capacity field). Use it when you have an immutable string that needs ownership but will never grow. Rc<str> and Arc<str> enable shared ownership of string data with reference counting.
String indexing by integer is disabled because UTF-8 characters vary in byte width: s[0] would return a byte, not a character, and could cut a multi-byte character in half. Use .chars().nth(0) for characters or .as_bytes()[0] for raw bytes.
Sources
- The Rust Book ; Storing UTF-8 Encoded Text with Strings
- std::string::String ; Rust Standard Library
- std::str ; Rust Standard Library
Related Glossary Terms
- Ownership:
Stringis owned;&stris always borrowed - Borrow Checker: Enforces that
&strreferences don't outlive their data - Lifetime:
&strin structs requires lifetime annotations - Generic:
Stringimplements many generic traits likeFrom<&str> - Regex: Regex engines mostly operate on
&strandStringinput - Reference:
Stringvs&stris fundamentally an ownership and reference distinction
