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.
chunks
Section titled “chunks”fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7];
for chunk in numbers.chunks(3) { println!("{chunk:?}"); }}[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.
chunks_exact and remainder
Section titled “chunks_exact and remainder”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:
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7];
let mut iter = numbers.chunks_exact(3); for chunk in iter.by_ref() { println!("{chunk:?}"); } println!("remainder: {:?}", iter.remainder());}[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.
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7]; let iter = numbers.chunks_exact(3); for chunk in iter { println!("{chunk:?}"); } println!("remainder: {:?}", iter.remainder());}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”.
The short tail is a trap
Section titled “The short tail is a trap”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]); }}36thread 'main' panicked at src/main.rs:4:24:index out of bounds: the len is 1 but the index is 2note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceThe 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.
In place with chunks_mut
Section titled “In place with chunks_mut”fn main() { let mut numbers = vec![1, 2, 3, 4, 5, 6];
for chunk in numbers.chunks_mut(2) { chunk[0] += 10; } println!("{numbers:?}");}[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.
Two more things
Section titled “Two more things”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-zeronote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceBoth 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.
Related
Section titled “Related”- What is the difference between a Vec and a slice? — the
&[T]each chunk hands you - How to get an element from a Vec without panicking —
get, includingchunk.get(2) - How to sort a Vec — the sort that often comes before batching
Next in this chapter: why can’t I use f64 as a HashMap key?