Slices

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.