Right, let’s talk about slices. You’ve met arrays ([T; N]) and you’ve met vectors (Vec<T>). They’re great. They own their data. But what if you don’t want ownership? What if you just want to borrow a view into a contiguous sequence of elements, be it from an array on the stack or a vector on the heap? You don’t want to copy the data; you just want to look at it, or maybe tell someone else where to look. That, my friend, is the slice type: &[T].

Think of a slice as a fat pointer. Under the hood, it’s not just a single memory address. It’s a two-part creature:

  1. A pointer to the start of the data block.
  2. The length of the slice (the number of elements).

This is why it’s so powerful and so fundamental. It gives you a length-checked, borrow-checked view into a sequence without any ownership overhead. It’s the fundamental building block for almost all data processing in Rust.

The Universal Borrower

You’ll almost never create a &[T] directly. Instead, you create one by borrowing an existing array or vector. The compiler is kind enough to perform a little magic called deref coercion. When you take a reference to a Vec<T> or an array, Rust will happily coerce it into a slice for you.

fn main() {
    // A vector (heap-allocated, growable)
    let my_vec: Vec<i32> = vec![1, 2, 3, 4, 5];

    // An array (stack-allocated, fixed-size)
    let my_array: [i32; 4] = [10, 20, 30, 40];

    // Borrowing a Vec<T> gives you a &[T]
    let vec_slice: &[i32] = &my_vec;
    // You can take a full slice or a partial one
    let partial_vec_slice = &my_vec[1..4]; // [2, 3, 4]

    // Borrowing an array gives you a &[T] too!
    let array_slice: &[i32] = &my_array;
    let partial_array_slice = &my_array[..2]; // [10, 20]

    // This function accepts any slice of i32s
    print_slice(vec_slice);
    print_slice(partial_array_slice);
}

// This function can operate on a slice from ANY contiguous i32 source.
fn print_slice(slice: &[i32]) {
    println!("Slice length: {}", slice.len());
    for element in slice {
        print!("{} ", element);
    }
    println!();
}

This is the killer feature. You can write functions that accept &[T] and they will work seamlessly with arrays, vectors, and even other slices. It’s the ultimate API for data inspection.

Slicing Syntax: It’s Not Magic, It’s Sugar

You’ve seen the [start..end] syntax. Let’s demystify it. The .. is a range operator. The end index is exclusive. This choice, while sometimes annoying if you’re used to inclusive ranges, is actually brilliant because slice.len() is always a valid end index.

let numbers = vec![0, 1, 2, 3, 4, 5];

// These are all &[i32]
let full = &numbers[..];        // [0, 1, 2, 3, 4, 5]
let from_start = &numbers[2..]; // [2, 3, 4, 5]
let to_end = &numbers[..4];     // [0, 1, 2, 3]
let middle = &numbers[2..4];    // [2, 3] <-- See? end is exclusive.
let everything = &numbers[0..numbers.len()]; // Same as `full`

The most important thing to remember: slicing operations are bounds-checked at runtime. Try &numbers[2..100] and your program will panic. There’s no undefined behavior here. It’s a clean, safe failure. This is Rust’s safety guarantee in action, and it’s worth the minimal performance cost for the vast majority of code.

Going Deeper: Methods and Gotchas

The slice type is packed with incredibly useful methods. You should definitely ctrl-click on &[T] in your IDE to explore them all, but here are the heavy hitters.

let mut digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let slice = &mut digits[..]; // Mutable slice: &mut [i32]

// Get the first element. Returns an Option<&T>.
let first = slice.first(); // Some(&0)
// Get the last element mutably. Option<&mut T>.
let last = slice.last_mut().unwrap(); // &mut 9
*last = 100;

// Check if it's empty
println!("Is empty? {}", slice.is_empty()); // false

// Get an element by index. Returns Option<&T>.
// Safer than using slice[index] which can panic.
let seventh = slice.get(7); // Some(&7)
let nonsense = slice.get(99); // None

// The workhorses: split_at and chunks
let (left, right) = slice.split_at(5);
// left is &[0, 1, 2, 3, 4], right is &[100, 6, 7, 8, 100] (wait, 100?)

// Chunks lets you iterate over... chunks.
for chunk in slice.chunks(3) {
    print!("{:?} ", chunk);
}
// Output: [0, 1, 2] [3, 4, 100] [6, 7, 8] [100]

Did you spot the “gotcha”? When I did slice.last_mut().unwrap(), I got a mutable reference to the last element, which was 9. I then changed it to 100. But my original array was [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]. The 5 was at index 5. So after the change, right became [100, 6, 7, 8, 100]. This is correct, but it’s a classic source of confusion—you have to keep mental track of which slice is looking at which part of the data.

The Golden Rule: Mutable Exclusivity

Here’s the single most important thing to remember about mutable slices (&mut [T]): Rust’s borrow checker ensures that you cannot have two mutable references that alias the same data. This applies to slices with a vengeance.

You cannot have two mutable slices that overlap. The methods are designed with this in mind. split_at_mut is the perfect example. It safely splits one mutable slice into two disjoint mutable slices by taking a mid-point index.

let all_data = &mut [1, 2, 3, 4, 5];
// This works because the borrow checker knows the slices don't overlap.
let (left, right) = all_data.split_at_mut(3);
left[0] = 10;   // left is &mut [1, 2, 3] -> becomes [10, 2, 3]
right[1] = 50;  // right is &mut [4, 5] -> becomes [4, 50]

// Trying to do this manually is a compile-time error:
// let slice_one = &mut all_data[..2];
// let slice_two = &mut all_data[1..3]; // ERROR! Cannot borrow `all_data` as mutable more than once at a time.

The compiler stops you from shooting yourself in the foot with iterator invalidation or data races. It feels restrictive at first, until you’ve debugged your tenth C++ vector iterator bug, at which point it feels like a superpower.

In short, &[T] is the lingua franca for sequence data in Rust. It’s safe, efficient, and incredibly versatile. Use it everywhere you don’t need ownership. Your function signatures will be cleaner and your code will be more robust for it.