How to split Vec into chunks

Use the chunks method to iterate over fixed-size groups or split_at_mut to divide a mutable vector into two distinct parts.

When you need pieces of a vector

You have a list of 10,000 user IDs. The API only accepts batches of 50. You have a grid of tiles to render, and your shader expects groups of four vertices. You have a mutable buffer of sensor readings, and you need to swap the first half with the second half for a signal processing trick. Splitting a collection into pieces is a daily task. Rust gives you several ways to do it, depending on whether you need ownership, mutability, or just a view.

Slices are windows, not copies

Rust separates the container from the view. A Vec<T> is the container that owns the memory. A slice (&[T]) is a view into that memory. When you split a vector, you almost never copy the data. You create slices that point to the original data. This is fast and cheap.

Imagine a long train of cargo cars. The Vec is the whole train. A slice is a window looking at a specific range of cars. chunks gives you a sliding window that shows you groups of cars. split_at drops a barrier so you can work on two sections independently. The train never moves. The cars never derail. You just change what you are looking at.

Slices are zero-cost views. Never copy data just to look at a piece of it.

Minimal examples

The chunks method returns an iterator over slices. Each slice has up to N elements. The last slice may be shorter if the length is not a multiple of N.

fn main() {
    let data = vec![10, 20, 30, 40, 50];

    // chunks returns an iterator of slices.
    // Each slice borrows from the original vector.
    // No data is copied.
    for chunk in data.chunks(2) {
        println!("{:?}", chunk);
    }
    // Output: [10, 20], [30, 40], [50]

    // split_at divides the slice at an index.
    // Returns two slices: left and right.
    // The index must be within bounds.
    let (left, right) = data.split_at(2);
    println!("Left: {:?}, Right: {:?}", left, right);
    // Output: Left: [10, 20], Right: [30, 40, 50]
}

split_at works on slices, not just vectors. You can call it on &v or &v[..]. The method panics if the index is out of bounds.

How the iterator moves

When you call chunks(2), Rust creates an iterator. The iterator holds a pointer to the start of the slice and the length. Each time you advance the iterator, it calculates the start and length of the next chunk using pointer arithmetic. No memory is allocated. No data is copied. The iterator yields slices that borrow from the original vector.

If you drop the iterator, the data stays alive because the Vec still owns it. The borrow checker ensures you cannot mutate the vector while the iterator is active. This prevents use-after-free errors.

split_at is even simpler. It calculates the midpoint and returns two slice references. The borrow checker ensures you don't use the original slice while holding the split parts. This prevents aliasing issues where two references point to the same memory.

Slices are windows, not copies. Use them to avoid allocation.

Realistic batch processing

Batch processing is a common pattern. You read data in chunks, process each chunk, and collect results. Pre-allocating the result vector avoids reallocations.

/// Process items in batches and return the sum of each batch.
fn process_batches(items: &[u32], batch_size: usize) -> Vec<u32> {
    // Pre-allocate capacity to avoid reallocations.
    // Add 1 for the potential remainder chunk.
    let mut results = Vec::with_capacity(items.len() / batch_size + 1);

    // Iterate over chunks.
    // Each chunk is a slice of up to batch_size elements.
    for chunk in items.chunks(batch_size) {
        let sum: u32 = chunk.iter().sum();
        results.push(sum);
    }

    results
}

fn main() {
    let data = vec![1, 2, 3, 4, 5, 6, 7];
    let sums = process_batches(&data, 3);
    // sums is [6, 9, 7]
    println!("{:?}", sums);
}

The community prefers Vec::with_capacity when the final size is predictable. It saves memory allocations and improves performance.

Pre-allocate when you know the size. Reallocation is a performance tax you can avoid.

Mutable chunks and in-place work

Sometimes you need to modify the data. chunks_mut returns an iterator over mutable slices. Each slice gives you exclusive access to a group of elements.

/// Normalize each chunk by dividing by its maximum value.
fn normalize_chunks(data: &mut [f32], chunk_size: usize) {
    // chunks_mut yields mutable slices.
    // Each slice is disjoint from the others.
    for chunk in data.chunks_mut(chunk_size) {
        let max = chunk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);

        // Avoid division by zero.
        if max > 0.0 {
            for val in chunk.iter_mut() {
                *val /= max;
            }
        }
    }
}

fn main() {
    let mut data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    normalize_chunks(&mut data, 2);
    // data is [0.5, 1.0, 0.66, 1.0, 1.0]
    println!("{:?}", data);
}

split_at_mut is the safe way to do in-place swaps or partitioning. It returns two mutable slices. The borrow checker enforces that the slices are disjoint. You cannot access the original vector while the split exists.

fn swap_halves(data: &mut [u32]) {
    let len = data.len();
    let mid = len / 2;

    // split_at_mut gives two mutable slices.
    // The slices are disjoint, so mutation is safe.
    let (left, right) = data.split_at_mut(mid);

    // Swap elements in place.
    for (l, r) in left.iter_mut().zip(right.iter_mut()) {
        std::mem::swap(l, r);
    }
}

The community prefers split_at_mut for in-place algorithms over indexing with &mut v[i]. Indexing requires the borrow checker to track individual elements, which can be harder to satisfy. split_at_mut gives you slices, and the compiler knows the slices are disjoint. This often makes complex mutations compile where indexing fails.

Use split_at_mut for in-place work. Indexing is a trap for complex mutations.

Splitting by value or predicate

chunks splits by size. split splits by a predicate. split returns an iterator over slices separated by elements that match the predicate. The separator elements are not included in the results.

fn main() {
    let data = vec![1, 2, 0, 3, 4, 0, 5];

    // split returns slices separated by zeros.
    // The zeros are not included.
    for part in data.split(|x| *x == 0) {
        println!("{:?}", part);
    }
    // Output: [1, 2], [3, 4], [5]

    // split_inclusive keeps the separator.
    // Useful for parsing lines with newlines.
    for part in data.split_inclusive(|x| *x == 0) {
        println!("{:?}", part);
    }
    // Output: [1, 2, 0], [3, 4, 0], [5]
}

split_inclusive is useful when the delimiter belongs to the chunk. For example, parsing text lines where the newline character stays with the line. split drops the delimiter. split_inclusive keeps it.

split_mut works like split but returns mutable slices. You can modify the parts independently.

Pick the split method that matches your delimiter needs. split drops separators. split_inclusive keeps them.

Pitfalls and compiler errors

chunks_exact is strict. It only yields chunks of exactly N elements. If the length is not a multiple of N, the remainder is ignored. This is useful for fixed-size records but dangerous if you expect all data.

fn main() {
    let data = vec![1, 2, 3, 4, 5];

    // chunks_exact drops the remainder.
    // Only yields [1, 2] and [3, 4].
    for chunk in data.chunks_exact(2) {
        println!("{:?}", chunk);
    }

    // Use remainder to access the tail.
    let remainder = data.chunks_exact(2).remainder();
    println!("Remainder: {:?}", remainder);
    // Output: [5]
}

split_at panics if the index is out of bounds. There is no safe fallible version on slices. Use get to check bounds first if the index comes from user input.

fn safe_split(data: &[u32], index: usize) -> Option<(&[u32], &[u32])> {
    // Check bounds before splitting.
    // get returns None if index is out of bounds.
    if data.get(index).is_some() {
        Some(data.split_at(index))
    } else {
        None
    }
}

If you try to use the original slice after split_at_mut, the compiler rejects you with E0502 (cannot borrow as mutable because it is also borrowed as immutable). The borrow checker prevents aliasing. You must finish using the split parts before accessing the original.

fn bad_usage(data: &mut [u32]) {
    let (left, right) = data.split_at_mut(2);
    left[0] = 10;

    // Error: E0502.
    // Cannot borrow data as immutable because it is also borrowed as mutable.
    println!("{:?}", data);
}

If the index is dynamic, check bounds before splitting. Panics are for bugs, not for user input.

Decision matrix

Use chunks when you need to iterate over fixed-size groups and the last group might be smaller.

Use chunks_exact when the data length is guaranteed to be a multiple of the chunk size, or when you want to ignore the remainder.

Use split_at when you need to divide a slice into two parts at a known index for read-only access.

Use split_at_mut when you need to modify two disjoint parts of a vector simultaneously, such as swapping halves or partitioning.

Use rchunks when processing from the end of the collection, like reading a stack or reversing a buffer.

Use split_inclusive when the delimiter element must belong to the chunk, such as parsing lines where the newline character stays with the line.

Reach for split or split_mut when you need to divide by a predicate or value rather than a fixed index or size.

Match the tool to the mutation. If you need two mutable halves, split_at_mut is your friend. If you just need to read, chunks does the heavy lifting.

Where to go next