Rust String Types: Rust Difference Between str and String
Rustβs primary UTF-8 string types are &str (borrowed, immutable) and String (owned, growable). Other string-like types exist for OS paths, FFI, and bytes.
| Type | Owned/Borrowed | UTF-8 Guaranteed | Null-Terminated | Typical Use | Key Conversions |
|---|---|---|---|---|---|
&str | Borrowed | Yes | No | Read-only string slices | s.to_string(), str::to_owned() |
String | Owned | Yes | No | Growable, heap-allocated strings | String::from(".."), my_string.as_str() |
&[u8] | Borrowed | No | No | Raw bytes | std::str::from_utf8(bytes) |
Vec<u8> | Owned | No | No | Owned byte buffer | String::from_utf8(vec)?, vec.as_slice() |
Cow<'a, str> | Borrowed/Owned | Yes | No | APIs that may borrow or allocate | Cow::Borrowed, Cow::Owned(String) |
CStr | Borrowed | No | Yes | Borrowed C strings (FFI) | CStr::from_ptr(ptr), .to_str()? |
CString | Owned | No | Yes | Owned C strings for FFI | CString::new("..")?, .as_c_str() |
OsStr | Borrowed | Platform-dep. | No | OS strings (paths, args) | .to_string_lossy(), .to_os_string() |
OsString | Owned | Platform-dep. | No | Owned OS strings | .into_string() (may fail), .as_os_str() |
Path | Borrowed | Platform-dep. | No | Filesystem paths | .to_str(), .as_os_str() |
PathBuf | Owned | Platform-dep. | No | Owned filesystem paths | .as_path(), .into_os_string() |
Problem
- You need to know when to use
&strvsString. - You need quick patterns for creating, converting, formatting, splitting, and multiline strings.
Solutions
Rust When to Use String vs str
- Use
&strto borrow read-only data. Cheap to pass around. - Use
Stringwhen you own the data or need to modify/grow it.
fn greet(name: &str) { // borrow: &str is ideal for params
println!("Hello, {name}!");
}
let owned: String = String::from("Ada");
greet(&owned); // &String -> &str
greet("Turing"); // &'static str
let mut s = String::from("Hi");
s.push_str(", there"); // need String to mutate
Create and Convert Between str and String
// &str -> String
let s1: String = "hello".to_string();
let s2: String = String::from("hello");
// String -> &str
let view: &str = s1.as_str(); // or &s1[..]
// Bytes <-> String (validate UTF-8)
let bytes: Vec<u8> = vec![97, 98, 99]; // a b c
let s_ok: String = String::from_utf8(bytes).unwrap();
let bad = vec![0xff, 0xfe];
let s_err = String::from_utf8(bad); // Err(FromUtf8Error)
Rust Strings (Basics)
let mut s = String::new();
s.push('A');
s.push_str("BC");
assert_eq!(s, "ABC");
let len_bytes = s.len(); // bytes, not chars
let first: char = s.chars().next().unwrap();
Rust Multiline String
let m = "line one
line two
line three";
let raw = r#"C:\path\no\escapes\needed "quotes" and \slashes"#;
Rust String Split
let csv = "a,b,c";
let parts: Vec<&str> = csv.split(',').collect();
assert_eq!(parts, ["a", "b", "c"]);
for tok in " x y ".split_whitespace() {
println!("{tok}");
}
Rust String Format
let user = "Ada";
let n = 3;
let msg = format!("Hello, {user}. You have {n} messages.");
println!("{msg}");
let hex = format!("{:02X}", 255); // "FF"
Things to Consider
Stringis UTF-8; length is in bytes, not chars.- Indexing
Stringwith[i]is not allowed. Use.chars(),.get(start..end), or iterators. &strslices must be on UTF-8 char boundaries. Invalid slices panic.- Prefer
&strin function parameters for flexibility. Accept both literals andString. - Use
Cow<'a, str>when an API may return either a borrowed&stror an ownedString. - Use
OsStr/OsStringfor filesystem/OS text; not guaranteed UTF-8. - Use
CStr/CStringfor FFI; NUL-terminated, no interior NULs.
Gotchas
Rustβs variety of string and path types can be confusing at first. Keep ownership and encoding in mind.
&str is a read-only view of UTF-8. It is cheap and ideal for parameters. String owns the data and can grow; use it when you need to build or mutate text. You can always borrow &str from a String via .as_str() or &s[..].
String is not indexable by integer because characters are variable-width in UTF-8. Slicing must respect character boundaries. Prefer iterating with .chars() or use .get(range) which returns Option<&str> and avoids panics.
OS and FFI strings are not regular UTF-8 text. Use OsStr/OsString for paths and command-line args; conversions to UTF-8 may fail or be lossy. Use CStr/CString at FFI boundaries and ensure no interior NUL bytes.
When splitting or formatting, remember that lengths and offsets are bytes. If you need character counts or grapheme clusters, use .chars() or a crate like unicode-segmentation. Allocate with String::with_capacity if you build big strings in a loop to reduce reallocations.
Sources
- The Rust Book: Strings
- std::primitive::str and std::string::String
- StackOverflow: Differences Between
Stringandstr - LogRocket: Understanding Rust
Stringvsstr - Rust
strTypes Overview
Further Investigation
CString/CStrfor FFI, interior-NUL rules, and conversions.OsStr/OsStringand platform differences (Windows vs Unix).unicode-segmentationcrate for grapheme-aware slicing.Cow<'a, str>patterns for zero-copy APIs.Path/PathBufusage and conversions.
TL;DR
Use &str to borrow read-only UTF-8. Use String to own/mutate text. Borrow &str from String when passing to functions.
fn takes_str(s: &str) { println!("{s}"); }
let mut owned = String::from("hello");
takes_str(&owned); // &String -> &str
owned.push_str(" world"); // need String to mutate
let bytes = owned.into_bytes(); // Vec<u8>
let back = String::from_utf8(bytes).unwrap();