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):
ptr: The memory address of the first element in the slice.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.