Slices

What Is a Slice

A slice is a borrowed view into a contiguous sequence of elements inside a larger collection—such as an array ([T; N]), vector (Vec<T>), or string (String). Rather than copying elements into a new buffer or taking ownership of the collection, a slice lets you read (or modify in place) any contiguous range of elements in $O(1)$ time.

How a Slice Views Memory (ptr + len)

At runtime, a slice reference (&[T], &mut [T], or &str) is a fat pointer consisting of two usize machine words (16 bytes total on 64-bit systems):

  1. ptr: The memory address of the first element in the slice.
  2. len: The number of elements (or UTF-8 bytes, for &str) in the slice.
Stack (`main`)                          Heap Buffer Owned by `vec![10, 20, 30, 40, 50]`
+-----------------------+               +------+------+------+------+------+
| numbers: Vec<i32>     |               |  10  |  20  |  30  |  40  |  50  |
|   ptr ────────────────+─────────────> +------+------+------+------+------+
|   len = 5, cap = 5    |                 [0]    [1]    [2]    [3]    [4]
+-----------------------+                         ^             ^
                                                  │             │
+-----------------------+                         │             │
| window: &[i32]        |                         │             │
|   ptr ────────────────+─────────────────────────┘             │
|   len = 3             | <─── Covers 3 elements (`20, 30, 40`)─┘
+-----------------------+

Creating a slice &numbers[1..4] does not allocate heap memory or loop over elements; it simply adds 1 * size_of::<i32>() to the base pointer and sets len = 3.

fn main() {
    let numbers: Vec<i32> = vec![10, 20, 30, 40, 50];

    // Borrow elements at index 1, 2, and 3 (`[20, 30, 40]`):
    let window: &[i32] = &numbers[1..4];

    println!("Full vector:  {numbers:?}");
    println!("Window slice: {window:?} (len = {})", window.len());
}
Full vector:  [10, 20, 30, 40, 50]
Window slice: [20, 30, 40] (len = 3)

The Two Primary Slice Types: &[T] and &str

Rust has two built-in slice types that work on the same fat-pointer principle:

Slice Type Underlying Unsized Type What It Views Can Borrow From
&[T] / &mut [T] [T] A contiguous sequence of T elements Arrays ([T; N]), vectors (Vec<T>), boxed slices (Box<[T]>)
&str / &mut str str A contiguous sequence of valid UTF-8 text bytes String literals ("hello"), owned String buffers
fn print_summary(items: &[u32], title: &str) {
    println!("{title} ({} items): {items:?}", items.len());
}

fn main() {
    let array_scores: [u32; 4] = [80, 85, 90, 95];
    let vec_scores: Vec<u32> = vec![100, 98, 96];
    let owned_title = String::from("Top Scores");

    // 1. Slice of a stack array + slice of an owned String:
    print_summary(&array_scores[1..3], &owned_title[0..3]);

    // 2. Full vector coerced to `&[u32]` + string literal `&str`:
    print_summary(&vec_scores, "All Vector Scores");
}
Top (2 items): [85, 90]
All Vector Scores (3 items): [100, 98, 96]

Borrow Checker Protection on Slices

Because a slice is a borrow (&[T] or &str) of its underlying owner (Vec<T>, [T; N], or String), the borrow checker guarantees that the underlying collection cannot be reallocated, cleared, or dropped while the slice is still in use:

// ❌ does not compile
fn main() {
    let mut words = String::from("hello world");

    // `first_word` borrows `words` immutably (`&str`):
    let first_word: &str = &words[0..5];

    // Error: `.clear()` requires `&mut words`, which would invalidate `first_word`!
    words.clear();

    println!("First word: {first_word}");
}
error[E0502]: cannot borrow `words` as mutable because it is also borrowed as immutable
 --> src/main.rs:8:5
  |
5 |     let first_word: &str = &words[0..5];
  |                             ----- immutable borrow occurs here
...
8 |     words.clear();
  |     ^^^^^^^^^^^^^ mutable borrow occurs here
9 |
10 |     println!("First word: {first_word}");
  |                            ---------- immutable borrow later used here

error: aborting due to 1 previous error

Without this check, words.clear() (or words.push_str(...)) could truncate or reallocate the buffer while first_word still pointed to the old bytes.