Rust Data Types

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 contiguous T elements 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:

  1. ptr: A pointer to the first element in the slice.
  2. len: A usize count 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).