Skip to content
Select theme

How to split a Vec into chunks

Short answer: vec.chunks(n) walks the Vec as &[T] slices of at most n elements. When the length is not a multiple of n, the last slice is short. chunks_exact(n) yields only full slices and hands the leftovers to remainder().

From a fresh project — cargo new chunks-demo — replace the contents of src/main.rs with each block below and run it with cargo run. The project name shows up only in cargo’s own output; nothing else here depends on it.

[1, 2, 3]
[4, 5, 6]
[7]

Seven elements in chunks of three: two full chunks, then one holding the leftover. Every item borrows the Vec — chunk is a &[i32], so nothing is copied and the Vec stays usable afterwards. The chunks never overlap, and an empty Vec produces no chunks at all.

When every chunk has to be full — fixed-size records, a batch of requests — chunks_exact leaves the tail out of the loop and returns it separately:

[1, 2, 3]
[4, 5, 6]
remainder: [7]

by_ref() is what keeps remainder() reachable: a plain for chunk in iter moves the iterator, and the call afterwards does not compile.

error[E0382]: borrow of moved value: `iter`
--> src/main.rs:7:33
|
3 | let iter = numbers.chunks_exact(3);
| ---- move occurs because `iter` has type `ChunksExact<'_, i32>`, which does not implement the `Copy` trait
4 | for chunk in iter {
| ---- `iter` moved due to this implicit call to `.into_iter()`
...
7 | println!("remainder: {:?}", iter.remainder());
| ^^^^ value borrowed here after move
|
note: the `for` loop is desugared into a call to `std::iter::IntoIterator::into_iter`, which takes ownership of the receiver `self`, which moves `iter`
--> /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/iter/traits/collect.rs:312:18
|
312 | fn into_iter(self) -> Self::IntoIter;
| ^^^^
For more information about this error, try `rustc --explain E0382`.

When the length divides evenly the remainder is empty, so the last chunk is full either way. The standard library also notes that because every chunk has exactly chunk_size elements, “the compiler can often optimize the resulting code better than in the case of chunks”.

Code that indexes into a chunk as if it were always full panics on the last one:

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7];
for chunk in numbers.chunks(3) {
println!("{}", chunk[2]);
}
}
3
6
thread 'main' panicked at src/main.rs:4:24:
index out of bounds: the len is 1 but the index is 2
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The first two chunks have three elements each; the third has one, and chunk[2] is past the end. Code that assumes a full chunk wants chunks_exact, or chunk.get(2) to stay in Option land.

[11, 2, 13, 4, 15, 6]

Each item is a &mut [i32], so the loop writes through it; the exclusive borrow ends when the chunk does. chunks_exact_mut is the same iterator with the length guarantee.

rchunks(3) starts at the back, so the short chunk comes first: [5, 6, 7], [2, 3, 4], [1]. A chunk size of zero panics:

fn main() {
let numbers = vec![1, 2, 3];
for chunk in numbers.chunks(0) {
println!("{chunk:?}");
}
}
thread 'main' panicked at src/main.rs:3:26:
chunk size must be non-zero
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Both chunks and chunks_exact are #[track_caller], so the panic points at your call rather than inside the standard library.

When the chunk size is a constant, as_chunks::<3>() returns (&[[i32; 3]], &[i32]) — every full chunk as an array, the tail as a slice — and skips the iterator. For a sliding view that overlaps, windows(2) is the call; chunks are disjoint.

Next in this chapter: why can’t I use f64 as a HashMap key?