Slices

Essential
Last updated: Tags: Rust

Prerequisites

An array [T; N] knows its length at compile time. But many algorithms need to operate on a run of elements whose length is only known at runtime — the result of a search, a chunk of a file, the contents of a Vec. Slices are how Rust models that.

[T] — the slice type

[T] (read: “slice of T”) is a contiguous sequence of T values of unknown length. Because its size is not known at compile time, [T] is a dynamically sized type (DST): you can never store a [T] directly in a variable, a struct field, or on the stack without some fixed-size wrapper.

// This would be a compile error — [i32] has unknown size:
// let s: [i32] = …;

In practice you almost always work with a slice reference: &[T] or &mut [T].

&[T] — the slice reference (a fat pointer)

A slice reference is a fat pointer: a pair of (address of the first element, number of elements). It occupies two usizes — twice the size of an ordinary reference.

use std::mem::size_of;

assert_eq!(size_of::<&[u8]>(), 2 * size_of::<usize>());

The address points into an existing contiguous allocation — an array, a Vec, a static buffer. The slice does not own the data; it borrows a view of it, exactly like references borrow scalar values.

&mut [T] — mutable slice reference

&mut [T] is the exclusive, read-write counterpart. It lets you sort, overwrite elements, or copy data into the slice. The same exclusivity rule as &mut T applies: no other reference to the overlapping data may exist at the same time.

let mut data = [3, 1, 4, 1, 5];
let s: &mut [i32] = &mut data;
s.sort();
// data is now [1, 1, 3, 4, 5]

Obtaining a slice

You can take a slice of any contiguous collection with a range index:

let arr = [10, 20, 30, 40, 50];
let all:   &[i32] = &arr[..];    // the whole array
let first: &[i32] = &arr[..3];   // [10, 20, 30]
let mid:   &[i32] = &arr[1..4];  // [20, 30, 40]

let v = vec![1, 2, 3, 4];
let chunk: &[i32] = &v[1..3];    // [2, 3]

Range syntax: .. (full), a..b (exclusive end), a..=b (inclusive end), ..b, a...

Indexing

Indexing a slice with s[i] returns the element by reference and panics at runtime if i is out of bounds:

let s = &[10, 20, 30][..];
let x = s[1]; // 20, because i32 is Copy

For fallible indexing, use .get(i), which returns Option<&T>:

match s.get(5) {
    Some(v) => println!("got {v}"),
    None    => println!("out of bounds"),
}

Prefer .get() whenever the index comes from untrusted input.

Iteration

Three common iteration patterns:

let s: &[i32] = &[1, 2, 3];

for x in s.iter() {         // yields &i32
    println!("{x}");
}

let mut data = [4, 5, 6];
for x in data.iter_mut() {  // yields &mut i32
    *x *= 2;
}
// data is now [8, 10, 12]

for x in [7, 8, 9] {        // consumes the array; yields i32 (Copy)
    println!("{x}");
}

For &[T] itself, a for x in s loop desugars to for x in s.iter() — it yields references, not values.

Common slice methods

let s: &[i32] = &[3, 1, 4, 1, 5];

s.len();          // 5
s.is_empty();     // false
s.first();        // Some(&3)
s.last();         // Some(&5)
s.contains(&4);   // true

let mut m = [3, 1, 4, 1, 5];
m.sort();         // [1, 1, 3, 4, 5] — sorts in place, requires &mut [T]

These methods are defined on [T] and are available through any slice reference without importing anything.

&str is a slice of UTF-8 bytes

A string slice &str is exactly &[u8] with the additional guarantee that the bytes are valid UTF-8. Everything said about &[T] applies: it is a fat pointer (address + length), it borrows data from somewhere else, and it does not own the string.

let greeting: &str = "hello";
let bytes: &[u8] = greeting.as_bytes(); // view the same data as raw bytes

The reason &str is not simply &[u8] is that UTF-8 characters can span multiple bytes. Indexing by byte position with s[i] is not available on &str — use .chars() to iterate over Unicode scalar values or .bytes() for raw bytes.

Array-to-slice coercion

An array [T; N] coerces to &[T] automatically wherever a slice reference is expected. This is called an unsized coercion:

fn sum(s: &[i32]) -> i32 {
    s.iter().sum()
}

let arr = [1, 2, 3, 4];
let total = sum(&arr); // &[i32; 4] coerces to &[i32]

Functions that accept &[T] therefore work equally well with arrays, Vec, and any other contiguous buffer — you don’t need separate overloads.

Summary

  • [T] is a DST; you always handle it through &[T] or &mut [T].
  • &[T] is a fat pointer: address + length, size = 2 * usize.
  • Obtain slices via range indexing: &arr[1..4], &v[..].
  • s[i] panics on out-of-bounds; .get(i) returns Option<&T>.
  • Iterate with .iter() (yields &T) or .iter_mut() (yields &mut T).
  • Useful methods: .len(), .is_empty(), .first(), .last(), .contains(), .sort().
  • &str is a slice of UTF-8 bytes — a fat pointer into a string buffer.
  • Arrays [T; N] coerce to &[T] automatically, so slice-accepting functions work with arrays too.