String Slices
A string slice (&str, pronounced "stir" or "string slice") is a borrowed, read-only view into a contiguous sequence of valid UTF-8 bytes stored somewhere in memory—whether inside the compiled binary's read-only data segment, inside a heap-allocated String, or on the stack.
Memory Layout of &str vs String
Just like an array slice &[u8], a string slice &str is a 16-byte fat pointer (on 64-bit systems) containing a memory address (ptr) and a byte length (len). By contrast, an owned String is a 24-byte header (ptr, len, cap) that owns a growable heap buffer:
Stack Binary Read-Only Data Segment (`.rodata`)
+------------------------+ +---+---+---+---+---+
| literal: &'static str | | H | e | l | l | o |
| ptr ─────────────────+───────────> +---+---+---+---+---+
| len = 5 |
+------------------------+
Stack Heap Buffer Owned by `owned`
+------------------------+ +---+---+---+---+---+---+---+---+---+---+---+
| owned: String | | R | u | s | t | | 2 | 0 | 2 | 4 | | |
| ptr ─────────────────+───────────> +---+---+---+---+---+---+---+---+---+---+---+
| len = 9, cap = 11 | ^ ^
+------------------------+ │ │
│ │
+------------------------+ │ │
| sub_slice: &str | │ │
| ptr ─────────────────+───────────────┘ │
| len = 4 | <─── Views `"Rust"` (4 bytes)─┘
+------------------------+
use std::mem::size_of;
fn main() {
let literal: &'static str = "Hello";
let owned: String = String::from("Rust 2024");
let sub_slice: &str = &owned[0..4];
println!("literal: '{literal}'");
println!("sub_slice: '{sub_slice}'");
println!("size_of::<&str>() = {} bytes", size_of::<&str>());
println!("size_of::<String>() = {} bytes", size_of::<String>());
}
literal: 'Hello'
sub_slice: 'Rust'
size_of::<&str>() = 16 bytes
size_of::<String>() = 24 bytes
String Literals Have Type &'static str
When you write a double-quoted string literal such as "Hello, world!", the compiler embeds those UTF-8 bytes directly into the executable binary file. At runtime, the literal evaluates to a &'static str slice pointing to that read-only memory, which remains valid for the entire duration of the program ('static).
Slicing a String by Byte Ranges (&s[start..end])
You can slice a String or &str using a byte range (start..end, ..end, start.., ..). Because &str must always contain valid UTF-8, both the start index and the end index must fall on valid UTF-8 character boundaries:
fn main() {
let phrase = "Ferris 🦀";
// "Ferris" = bytes 0..6
// " " = byte 6..7
// "🦀" = bytes 7..11 (4-byte UTF-8 emoji)
let name: &str = &phrase[..6];
let emoji: &str = &phrase[7..];
println!("name = '{name}', emoji = '{emoji}'");
// Check whether an index is a valid UTF-8 boundary before slicing:
println!("Is byte 7 a char boundary? {}", phrase.is_char_boundary(7));
println!("Is byte 8 a char boundary? {}", phrase.is_char_boundary(8));
// Or use `.get(range)` to return `None` instead of panicking on mid-char indices:
println!("phrase.get(7..9) = {:?}", phrase.get(7..9));
}
name = 'Ferris', emoji = '🦀'
Is byte 7 a char boundary? true
Is byte 8 a char boundary? false
phrase.get(7..9) = None
Zero-Allocation Text Processing with &str Methods
Because &str methods return borrowed sub-slices (&str) pointing into the original string buffer, you can trim whitespace, strip prefixes, and split lines or words with zero heap allocations:
| Method | Example on s = " key=value\n" |
Returns (Zero Heap Allocation!) |
|---|---|---|
s.trim() / s.trim_start() |
s.trim() |
"key=value" (&str) |
s.split_once(pat) |
"key=value".split_once('=') |
Some(("key", "value")) (Option<(&str, &str)>) |
s.strip_prefix(p) |
"https://x".strip_prefix("https://") |
Some("x") (Option<&str>) |
s.lines() |
s.lines() |
Iterator yielding each line as &str |
s.split_whitespace() |
s.split_whitespace() |
Iterator yielding each whitespace-separated word as &str |
s.starts_with(p) / s.contains(p) |
s.contains('=') |
true (bool) |
fn first_word(text: &str) -> &str {
match text.trim().split_once(' ') {
Some((first, _rest)) => first,
None => text.trim(),
}
}
fn main() {
let sentence = String::from(" Rust string slices never allocate ");
let word: &str = first_word(&sentence);
println!("First word: '{word}'");
}
First word: 'Rust'
Why Functions Should Accept &str Instead of &String
Because String implements Deref<Target = str>, passing &my_string to a function expecting &str converts it to &str automatically at zero cost. By contrast, a function taking &String rejects string literals ("hello") and sub-slices unless the caller allocates a brand-new String first! Always use &str for read-only string parameters.