Slice Types
A slice (&[T] or &mut [T]) is a dynamically sized view into a contiguous sequence of elements of type T stored in an array ([T; N]), vector (Vec<T>), or another buffer. Unlike an array, a slice does not own the elements it points to, and its length is stored at runtime rather than fixed in its compile-time type signature.
Unsized [T] vs Fat-Pointer Slice References (&[T] and &mut [T])
In Rust's type grammar:
- Bare
[T]is a Dynamically Sized Type (DST): it represents an unknown number of contiguousTelements in memory. Because its byte size is not known at compile time, a bare[T]cannot be stored directly in a local variable (let x: [i32] = ...does not compile). - Instead, slices are always used behind a reference—either a shared slice (
&[T], read-only view) or a mutable slice (&mut [T], read-write view).
Fat Pointer Memory Layout (ptr + len)
While an ordinary reference (&i32 or &[i32; 4]) is a single machine word (8 bytes on 64-bit systems) storing only a memory address, a slice reference (&[T] or &mut [T]) is a two-word fat pointer (16 bytes on 64-bit systems) containing:
ptr: A pointer to the first element in the slice.len: Ausizecount of how many elements belong to the slice.
Stack Contiguous Memory (Stack Array or Heap Vec)
+------------------------+ +------+------+------+------+------+
| array_data: [i32; 5] | | 10 | 20 | 30 | 40 | 50 |
+------------------------+ +------+------+------+------+------+
^ ^
+------------------------+ | |
| middle: &[i32] | | |
| ptr -----------------+----------------------+ |
| len = 3 | <---- (Covers elements 20, 30, 40)-+
+------------------------+
(16 bytes on 64-bit)
use std::mem::size_of;
fn main() {
let numbers: [i32; 5] = [10, 20, 30, 40, 50];
// Borrow elements at indices 1, 2, and 3 (`[20, 30, 40]`):
let middle: &[i32] = &numbers[1..4];
println!("middle slice: {middle:?} (len = {})", middle.len());
println!("size_of::<&[i32; 5]>() (thin pointer) = {} bytes", size_of::<&[i32; 5]>());
println!("size_of::<&[i32]>() (fat pointer) = {} bytes", size_of::<&[i32]>());
}
middle slice: [20, 30, 40] (len = 3)
size_of::<&[i32; 5]>() (thin pointer) = 8 bytes
size_of::<&[i32]>() (fat pointer) = 16 bytes
Creating Slices from Arrays and Vectors
Because &[T; N] and &Vec<T> both automatically coerce (via unsized coercion and Deref) to &[T], you can create a slice either by passing &array / &vec directly to a function expecting &[T] or by indexing with a range (&collection[start..end]):
fn sum_elements(slice: &[i32]) -> i32 {
let mut total = 0;
for &item in slice {
total += item;
}
total
}
fn main() {
let fixed_array: [i32; 4] = [1, 2, 3, 4];
let heap_vector: Vec<i32> = vec![10, 20, 30, 40, 50];
// ONE function `sum_elements(&[i32])` accepts full arrays, full vectors, and sub-slices!
println!("Full array: {}", sum_elements(&fixed_array));
println!("Full vector: {}", sum_elements(&heap_vector));
println!("Sub-slice [1..4]: {}", sum_elements(&heap_vector[1..4]));
}
Full array: 10
Full vector: 150
Sub-slice [1..4]: 90
This is why idiomatic Rust functions almost always accept &[T] rather than &[T; N] or &Vec<T>: a single &[T] parameter works with stack arrays of any size, heap vectors, and sub-slices without copying a single byte.
Modifying Elements Through a Mutable Slice (&mut [T])
A mutable slice &mut [T] cannot change the length of the underlying array or vector (you cannot push or remove elements through &mut [T]), but it can modify, reorder, swap, or sort the existing elements in place:
fn zero_negatives_and_sort(slice: &mut [i32]) {
for value in slice.iter_mut() {
if *value < 0 {
*value = 0;
}
}
slice.sort();
}
fn main() {
let mut readings = [15, -4, 8, -12, 3];
// Pass a mutable slice covering the middle 3 elements (`[-4, 8, -12]`):
zero_negatives_and_sort(&mut readings[1..4]);
println!("Updated array: {readings:?}");
}
Updated array: [15, 0, 0, 8, 3]
Essential Slice Methods and Pattern Matching
Slices provide a rich set of zero-allocation inspection and splitting methods:
| Method | Example on s = &[10, 20, 30, 40] |
Returns |
|---|---|---|
s.len() / s.is_empty() |
s.len() |
4 / false |
s.first() / s.last() |
s.first() |
Some(&10) / Some(&40) |
s.split_first() |
s.split_first() |
Some((&10, &[20, 30, 40])) |
s.split_at(mid) |
s.split_at(2) |
(&[10, 20], &[30, 40]) |
s.chunks(size) |
s.chunks(2) |
Iterator yielding &[10, 20], then &[30, 40] |
s.windows(size) |
s.windows(2) |
Overlapping sliding windows: &[10, 20], &[20, 30], &[30, 40] |
s.binary_search(&x) |
s.binary_search(&30) |
Ok(2) (on sorted slice) |
s.starts_with(prefix) |
s.starts_with(&[10, 20]) |
true |
Slice Patterns in match and if let
You can match the length and structure of a slice directly using [...] slice patterns and the rest pattern ..:
fn describe_slice(slice: &[i32]) {
match slice {
[] => println!("Empty slice"),
[only] => println!("Single element: {only}"),
[first, second] => println!("Pair: {first} and {second}"),
[first, middle @ .., last] => {
println!("Starts with {first}, ends with {last}, middle = {middle:?}");
}
}
}
fn main() {
describe_slice(&[]);
describe_slice(&[42]);
describe_slice(&[1, 2, 3, 4, 5]);
}
Empty slice
Single element: 42
Starts with 1, ends with 5, middle = [2, 3, 4]
String Slices (&str)
The string slice type &str is the text counterpart of &[u8]: it is a 16-byte fat pointer (ptr + byte len) that points to a contiguous sequence of valid UTF-8 bytes in memory (either baked into the binary's read-only data segment for string literals like "hello", or inside a heap-allocated String).