Slices, Chunks, and Windows
A slice (&[T]) is the most-used borrowed type in Rust after &str, and like &str it's a view: a pointer plus a length, borrowing into data owned elsewhere. Collections Deep Dive covered the owning containers; this tutorial covers the view you pass around instead. The payoff is twofold, accepting &[T] makes a function work with Vec, arrays, and other slices alike, and the slice API (chunks, windows, split_at, binary_search) replaces a lot of error-prone manual indexing with methods that can't go out of bounds.
Accept &[T], Not &Vec<T>
The single most common slice mistake is taking &Vec<T> as a parameter. It needlessly restricts callers, a &Vec<T> accepts only a Vec, while &[T] accepts a Vec, an array, another slice, or anything that derefs to a slice:
// too restrictive: only a Vec works
fn sum(values: &Vec<i32>) -> i32 {
values.iter().sum()
}
// idiomatic: works with Vec, arrays, slices, all of them
fn sum(values: &[i32]) -> i32 {
values.iter().sum()
}
sum(&vec![1, 2, 3]); // &Vec coerces to &[i32] via Deref
sum(&[1, 2, 3]); // array coerces too
sum(&data[1..4]); // a sub-slice
This is the same lesson as "accept &str, not &String" from Strings: Choosing the Right Type, applied to collections: a Vec<T> derefs to &[T] (via the Deref mechanism from Deref, AsRef, and Borrow), so taking the slice type costs callers nothing and frees them from being forced to own a Vec. Only take &Vec<T> when you genuinely need Vec-specific methods (you almost never do).
Indexing: get() vs []
Direct indexing (slice[i]) panics on an out-of-bounds index, the same as it does for Vec. When the index might be invalid, get() returns an Option instead, turning a potential crash into a value you handle:
let data = [10, 20, 30];
let x = data[5]; // PANIC: index out of bounds
let y = data.get(5); // None — no panic
if let Some(v) = data.get(5) {
println!("{v}");
}
Gotcha: reach for
.get()whenever the index comes from anything you don't control, user input, a computed offset, a parsed value, so a bad index is a handledNonerather than a process-killing panic. Reserve bareslice[i]for indices you can prove are in range (a loop bound by.len(), a constant). The.first(),.last(),.split_first(), and.split_last()helpers are the panic-free way to peel off ends, all returnOption, so an empty slice is handled rather than crashing.
chunks and windows: Two Different Groupings
Two slice methods produce sub-slices, and they're easy to confuse because the difference is subtle but important. chunks(n) splits into non-overlapping groups of n (the last may be shorter); windows(n) produces overlapping sliding views of width n.
let data = [1, 2, 3, 4, 5];
// chunks(2): non-overlapping — for batching, pairing, grid rows
for c in data.chunks(2) {
println!("{c:?}"); // [1, 2] [3, 4] [5]
}
// windows(2): overlapping — for comparing adjacent elements, moving averages
for w in data.windows(2) {
println!("{w:?}"); // [1, 2] [2, 3] [3, 4] [4, 5]
}
Use chunks to partition data (process records in batches of 100, lay out a matrix row by row); use windows to look at adjacent relationships (is the sequence sorted? compute deltas between neighbors). A common idiom: data.windows(2).all(|w| w[0] <= w[1]) checks whether a slice is sorted in one line. (chunks_exact(n) is a faster variant when you want to drop any short final chunk and let the optimizer assume a fixed width.)
Splitting Without Copying
split_at(mid) divides a slice into two views at an index, no allocation, both halves borrow the original. The &mut version, split_at_mut, is the sanctioned way to get two mutable sub-slices of the same data, which the borrow checker otherwise forbids (Working With the Borrow Checker):
let mut data = [1, 2, 3, 4, 5, 6];
let (left, right) = data.split_at_mut(3); // two non-overlapping &mut slices
left[0] = 100;
right[0] = 200; // both mutable at once — legal because they can't overlap
This is exactly the pattern the unsafe tutorial showed std's split_at_mut implementing internally: a safe API over a provably-non-overlapping split. There's also split() / splitn() for dividing on a predicate (like str::split but for any &[T]), and split_first()/split_last() for peeling one element off an end while keeping the rest as a slice, the slice-native way to write head/tail recursion.
binary_search: Logarithmic Lookup on Sorted Data
On a sorted slice, binary_search finds an element in O(log n) instead of the O(n) of a linear contains/iter().position(). Its return type is the clever part: Result<usize, usize>, Ok(i) if found (at index i), and Err(i) if not, where i is where it would be inserted to keep the slice sorted.
let sorted = [10, 20, 30, 40, 50];
match sorted.binary_search(&30) {
Ok(i) => println!("found at {i}"), // Ok(2)
Err(i) => println!("would insert at {i}"),
}
// the Err index is directly usable to keep a Vec sorted on insert:
let mut v = vec![10, 20, 40];
let target = 30;
if let Err(pos) = v.binary_search(&target) {
v.insert(pos, target); // v is now [10, 20, 30, 40], still sorted
}
Gotcha:
binary_searchis only correct on a slice that's already sorted by the same ordering, on unsorted data it returns meaningless results, silently, with no error. There's no check that the precondition holds; it's on you. If the data isn't kept sorted, aHashSet(Collections Deep Dive) for membership or a linear scan is the safer choice. TheErr(insertion_point)return is what makes a sortedVeca viable ordered structure, but only as long as every insert goes throughbinary_searchto preserve the invariant.
Slice Method Cheat Sheet
| You want | Use |
|---|---|
A function that accepts Vec/array/slice | take &[T] (not &Vec<T>) |
| Indexing that can't panic | .get(i) → Option, .first()/.last() |
| Non-overlapping batches | .chunks(n) / .chunks_exact(n) |
| Overlapping sliding views | .windows(n) |
| Two mutable halves of one slice | .split_at_mut(mid) |
| Peel one element off an end | .split_first() / .split_last() |
| Fast lookup on sorted data | .binary_search(&x) (sorted only!) |
| Split on a predicate/separator | .split(...) / .splitn(...) |
The throughline: the slice API is designed so the common operations, grouping, splitting, searching, are methods that respect bounds, not manual for i in 0..len loops where an off-by-one becomes a panic or a bug. Prefer the named method; it states intent and stays in bounds by construction.
Key Takeaways
- A slice
&[T]is a borrowed view (pointer + length). Accept&[T]in function parameters, not&Vec<T>, so callers can pass aVec, array, or sub-slice without being forced to own aVec. slice[i]panics out of bounds; use.get(i)(returnsOption) for any index you don't control, and.first()/.last()/.split_first()to handle ends without crashing on empty.chunks(n)gives non-overlapping batches (partitioning);windows(n)gives overlapping sliding views (adjacent comparisons). Don't confuse them,windows(2).all(...)is the one-liner sortedness check.split_at_mutis the safe way to get two mutable sub-slices of the same data;split/split_first/split_lastdivide without copying.binary_searchis O(log n) on sorted slices and returnsResult<usize, usize>, theErrindex is the insertion point that keeps aVecsorted. It silently misbehaves on unsorted data, so the sorted invariant is your responsibility.