The .windows(size)
method in Rust returns an iterator that yields overlapping slices, where each slice has a length defined by the size parameter.
Each window is a slice that starts one element after the previous window, and all windows are references into the original slice. This behavior contrasts with .chunks(size)
, which produces non-overlapping slices instead of overlapping ones.
The .windows(size)
method is defined on the slice type &[T]
, but you can also call it on a vector Vec<T>
. This works because Vec<T>
implements Deref<Target = [T]>
, allowing it to automatically dereference to a slice when needed. In other words, when you call a slice method on a vector, Rust automatically dereferences it to a slice &[T]
through the Deref trait (Rust performs an automatic coercion that temporarily treats the vector as a slice).
Internally, .windows(size)
uses pointer arithmetic. A slice is stored in memory as a pointer to its first element and its length. As the iterator advances, Rust shifts this pointer by one element at a time and creates a new sub-slice from the original slice or vector.
For example:
When you call .windows(3)
on a vector nums = [10, 20, 30, 40, 50]
, Rust first takes [10, 20, 30]
as the initial window. Then it moves the starting position forward by one element each time, producing the next windows [20, 30, 40]
and [30, 40, 50]
. Each window is an overlapping view of length 3 into the same nums
vector. You can see this flow in the code below:
fn main() {
let nums = [10, 20, 30, 40, 50];
for window in nums.windows(3) {
println!("{:?}", window);
}
}
Output:
[10, 20, 30]
[20, 30, 40]
[30, 40, 50]
Visualizing the windows model layout
Let’s model the slice nums
( from our previous example) in memory using the diagram below as a starting point.
Index: 0 1 2 3 4
Value: [10] [20] [30] [40] [50]
Ptr: ^
.windows(3)
means: start the pointer at index 0, take 3 elements, yield a reference [10, 20, 30]
. Then move the pointer forward by 1 and take another view. Here is a visual representation of that process.
Index: 0 1 2 3 4
Value: [10] [20] [30] [40] [50]
Window 1: 10 20 30
Window 2: 20 30 40
Window 3: 30 40 50
Internal implementation
To further understand how .windows(size)
is implemented under the hood, let’s look at a simplified version of its source code from the Rust standard library.
From the code below, the public .windows(size)
method first checks that the window size isn’t zero, then returns a Windows
iterator.
// The public method
pub fn windows(&self, size: usize) -> Windows<'_, T> {
assert!(size != 0);
Windows::new(self, NonZero::new(size).unwrap())
}
The Windows itself is a struct. It holds two pieces of data: a reference to the slice being iterated over (v), and the window size (size). The lifetime 'a
ensures each window reference is tied to the lifetime of the original slice.
// The iterator struct
pub struct Windows<'a, T> {
v: &'a [T],
size: NonZero<usize>,
}
Finally, here’s a simplified version of how the window iterator itself is implemented. Each call to next() checks if there are enough elements left for a full window. If so, it creates a slice reference for the current window, then advances the internal slice pointer by one element (creating the overlap). If there aren’t enough elements remaining, it returns None to signal the iteration is complete.
// The Iterator implementation (simplified)
impl<'a, T> Iterator for Windows<'a, T> {
type Item = &'a [T];
fn next(&mut self) -> Option<Self::Item> {
if self.v.len() >= self.size.get() {
let window = &self.v[..self.size.get()];
self.v = &self.v[1..]; // Advance by one element
Some(window)
} else {
None
}
}
}
Counting the number of windows
If the slice has length len
and the window size is n
, the total number of windows is:
len - n + 1
For example, with [10, 20, 30, 40, 50]
(length 5) and n = 3:
5 - 3 + 1 = 3 windows
This matches what we saw earlier.
What happens when the size Is too large or zero
Two special cases are worth acknowledging:
1. If size > len, you get no windows:
let v = [1, 2];
assert_eq!(v.windows(3).count(), 0); // .count() is syntatic sugar for v.len()
Because there’s not enough data to fill one full window.
2. If size == 0, Rust panics:
let v = [1, 2, 3];
v.windows(0); // panic!
A zero-length window doesn’t make logical sense, and it’s disallowed to avoid edge cases.
Verifying if a vector is sorted
This example will demonstrate how .windows
work in practice. We’ll also show how using .windows()
compares to manual indexing.
The code below has a vector of i32
and a function is_sorted
(which is not implemented yet). The goal is to check if the vector passed to the is_sorted
function is actually sorted in ascending order. If it’s sorted, we’ll return true
, otherwise, we’ll return false
.
fn main() {
let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
let result = is_sorted(v);
println!("{}", result);
}
pub fn is_sorted(v: Vec<i32>) -> bool {
// We’ll fill this in.
true
}
We’ll use two different approaches. The first and most common approach is indexing.
Using vector indexing to verify if a vector is sorted
The code below checks every adjacent pair of the elements in the vector: v[i]
and v[i + 1]
.
If a previous element is greater than the next one, the list isn’t sorted.
pub fn is_sorted(v: Vec<i32>) -> bool {
for i in 0..v.len() - 1 {
if v[i] > v[i + 1] {
return false;
}
}
true
}
Let’s trace how it works:
v = [1, 2, 3, 4]
^ ^
| |
i i+1
Comparisons:
1 < 2
2 < 3
3 < 4
Each iteration compares the current element with the next one.
But that’s exactly what .windows(2)
gives us.
Using .windows(size)
to verify if a vector is sorted
Now let’s rewrite the same logic using .windows(size)
:
pub fn is_sorted(v: Vec<i32>) -> bool {
for window in v.windows(2){
if window[0] > window[1]{
return false
}
}
true
}
Let’s visualize what it produces for [1, 2, 3, 4]
:
Window 1: [1, 2]
Window 2: [2, 3]
Window 3: [3, 4]
For each window, window[0]
is the current element and window[1]
is the next one. That means for Window 1: [1, 2]
, window[0] would be 1
while window[1] would be 2
We check that each window[0] <= window[1]
. If all comparisons hold, the list is sorted.
.windows() vs .chunks()
.chunks(2)
divides the slice into non-overlapping parts.
.windows(2)
slides the view by one each time.
let v = [1, 2, 3, 4];
println!("{:?}", v.chunks(2).collect::<Vec<_>>());
println!("{:?}", v.windows(2).collect::<Vec<_>>());
Output:
[[1, 2], [3, 4]]
[[1, 2], [2, 3], [3, 4]]
Exercise
To solidify your understanding of .windows
implement your own version of it. You can call it my_windows
. You can’t name it windows
since that would conflict with Rust’s standard library implementation.
Conclusion
The .windows(size)
method is a clean way to work with overlapping parts of a slice or vector in Rust. It avoids manual indexing and makes many algorithms easier to express, such as checking if a list is sorted or finding patterns in a sequence. Under the hood, it works through pointer arithmetic, shifting the slice’s start position one element at a time to create new views into the same data.