Wenn du in einer Sequenz nach gleitenden Fenstern oder festen Blöcken suchen willst, sind windows und chunks die zwei Iterator-Adapter, die du brauchst. windows(n) liefert überlappende Fenster der Länge n — perfekt für Pattern-Suche, Bigram-Analyse, Sliding-Average. chunks(n) liefert disjunkte Blöcke — ideal für Block-weise Verarbeitung wie SIMD-Vektorisierung, Bild-Tiles, Audio-Frames. Dazu kommt chunks_exact für garantierte Block-Längen und chunks_mut für In-Place-Verarbeitung. Dieser Artikel zeigt alle Varianten mit konkreten Praxis-Beispielen.
windows(n) — überlappende Fenster
windows(n) gibt einen Iterator über überlappende Fenster der Länge n zurück:
fn main() {
let v = [1, 2, 3, 4, 5];
for fenster in v.windows(3) {
println!("{fenster:?}");
}
// [1, 2, 3]
// [2, 3, 4]
// [3, 4, 5]
}Im Beispiel siehst du das Gleitverhalten: das erste Fenster umfasst die ersten drei Elemente, das zweite verschiebt sich um eins (überlappt mit dem ersten in zwei Elementen), das dritte ebenso. Bei einem 5-elementigen Slice und Fenstergröße 3 ergeben sich genau 3 Fenster — allgemein L - n + 1.
Eigenschaften
windows hat ein paar Eigenheiten, die du kennen musst:
Längen-Garantie: jedes ausgegebene Fenster hat exakt n Elemente. Das ist anders als bei chunks, wo der letzte Block kürzer sein kann. Damit kannst du im Loop-Body bedenkenlos mit fester Länge arbeiten — Slice-Patterns wie [a, b, c] greifen verlässlich.
Bei n == 0: Panic. Eine Fenstergröße von Null macht keinen Sinn. Bei dynamischem n vorher prüfen.
Bei Slice kürzer als n: leerer Iterator. Das ist sicher (kein Panic), aber du musst den Fall bei der Schleifen-Logik beachten — windows auf einem zu kurzen Slice gibt einfach gar keine Iteration.
Read-only: windows gibt &[T] zurück, nicht &mut [T]. Es gibt kein windows_mut in der Stdlib — und das ist Absicht: zwei mutable Borrows auf überlappende Bereiche wären eine Aliasing-Verletzung. Wer überlappend mutieren will, muss mit Indizes arbeiten.
chunks(n) — disjunkte Blöcke
chunks(n) gibt einen Iterator über nicht-überlappende Blöcke der Länge n zurück. Der letzte Block kann kürzer sein, wenn die Gesamtlänge kein Vielfaches von n ist.
fn main() {
let v = [1, 2, 3, 4, 5, 6, 7];
for block in v.chunks(3) {
println!("{block:?}");
}
// [1, 2, 3]
// [4, 5, 6]
// [7] ← letzter Block ist kürzer
}Während windows Überlappung erlaubt, sind die Blöcke von chunks disjunkt: zwischen ihnen gibt es keine Schnittmenge, jedes Element der Eingabe gehört zu genau einem Block. Damit eignet sich chunks für Block-weise Verarbeitung wie Bild-Tiles, Audio-Frames, Network-Pakete, Batch-Sends.
Der Haken: wenn die Gesamtlänge nicht durch n teilbar ist, ist der letzte Block kürzer. Bei einem 7-elementigen Slice und Block-Größe 3 gibt es zwei vollständige Blöcke (jeweils 3 Elemente) und einen Rest-Block mit 1 Element. Du musst im Loop-Body damit rechnen oder zu chunks_exact greifen, das diesen Rest separat behandelt.
chunks_exact(n) — garantierte Blöcke
Wenn du garantiert Blöcke der Länge n willst (und Rest-Bytes separat behandeln möchtest), nutzt du chunks_exact:
fn main() {
let v = [1, 2, 3, 4, 5, 6, 7];
let chunks = v.chunks_exact(3);
let rest = chunks.remainder(); // letzte 1 Element
for block in chunks {
println!("{block:?}");
}
println!("Rest: {rest:?}");
// [1, 2, 3]
// [4, 5, 6]
// Rest: [7]
}chunks_exact ist die Performance-bewusste Variante. Drei Eigenschaften zeichnen sie aus:
Jeder iterierte Block hat exakt n Elemente — kein Sonderfall für den letzten. Im Loop-Body kannst du sicher mit block[0], block[1], ..., block[n-1] arbeiten, ohne Längen-Prüfung.
remainder() gibt die nicht zu einem vollen Block gehörenden Elemente separat zurück. Du kannst sie nach der Schleife mit anderer Logik behandeln — etwa „verwerfen", „padden", „separat verarbeiten".
Compiler-freundlich: bei festen Block-Größen wie 4, 8 oder 16 erkennt der LLVM-Optimizer oft, dass die Schleife vektorisierbar ist, und generiert SIMD-Instruktionen. Damit läuft die Verarbeitung 2-4× schneller als bei chunks — ohne dass du selbst Vektor-Operationen schreiben musst.
Für performance-kritische Block-Verarbeitung in numerischem Code, Bild-/Audio-Pipelines und Krypto ist chunks_exact deshalb die idiomatische Wahl.
Mutable Varianten
chunks_mut
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
for block in v.chunks_mut(2) {
// block ist &mut [i32]
for x in block.iter_mut() {
*x *= 10;
}
}
assert_eq!(v, vec![10, 20, 30, 40, 50, 60]);
}chunks_exact_mut
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let mut iter = v.chunks_exact_mut(2);
for block in &mut iter {
block[0] += 100;
block[1] += 200;
}
let rest = iter.into_remainder(); // letzter Wert
for x in rest {
*x = 0;
}
assert_eq!(v, vec![101, 202, 103, 204, 0]);
}Keine windows_mut
Es gibt bewusst kein windows_mut in der Stdlib — überlappende mutable Slices wären Aliasing-Konflikte. Wer ähnliches braucht: über Indices iterieren mit split_at_mut-Tricks oder spezialisierte Crates.
rchunks und rwindows
Beide Adapter haben rückwärts-Varianten — Iteration vom Ende her:
fn main() {
let v = [1, 2, 3, 4, 5, 6, 7];
for block in v.rchunks(3) {
println!("{block:?}");
}
// [5, 6, 7]
// [2, 3, 4]
// [1]
// (rückwärts gruppiert!)
}Nützlich, wenn die natürliche Lese-Richtung das Ende ist (z. B. Hash-Vergleiche von hinten, Multi-Byte-Integer-Lesen).
Vergleich windows vs. chunks
windows(n) | chunks(n) | |
|---|---|---|
| Überlappung | ja, je n-1 Elemente | nein |
| Anzahl Iterationen | L - n + 1 | ceil(L / n) |
| Längen-Garantie | jeder Block n | letzter kann kürzer |
| Mutable Variante | nein (Aliasing) | ja (chunks_mut) |
| Typische Anwendung | Pattern-Suche, Sliding-Average | Block-weise Verarbeitung |
| Edge Case bei kurzem Slice | leerer Iterator | letzter Block kürzer |
Praxis: windows und chunks im echten Code
Pattern-Suche in Bytes (windows)
pub fn finde_pattern(daten: &[u8], pattern: &[u8]) -> Option<usize> {
daten.windows(pattern.len())
.position(|fenster| fenster == pattern)
}
fn main() {
let data = b"Hallo Welt mit Magic-PNG-Header: \x89PNG\r\n\x1a\n weiter";
let png_magic = b"\x89PNG\r\n\x1a\n";
assert_eq!(finde_pattern(data, png_magic), Some(33));
}Eine klassische Substring-/Sub-Slice-Suche. windows(pattern.len()) produziert alle Sub-Slices der passenden Länge, position(|w| w == pattern) findet die erste passende Stelle. Die Komplexität ist O(n*m) — für jeden möglichen Start-Index wird der Pattern vollständig verglichen.
Bei großen Texten gibt es effizientere Algorithmen (Boyer-Moore, Knuth-Morris-Pratt mit Pre-Computation), aber für die meisten Anwendungen ist diese naive Variante schnell genug und kommt ohne externe Crates aus. Die Stdlib hat auch &[u8]::contains_slice als spezialisierte Methode für den „existiert irgendwo?"-Fall.
Sliding-Window-Average (windows)
pub fn moving_average(daten: &[f64], fenstergroesse: usize) -> Vec<f64> {
if daten.len() < fenstergroesse { return Vec::new(); }
daten.windows(fenstergroesse)
.map(|w| w.iter().sum::<f64>() / fenstergroesse as f64)
.collect()
}
fn main() {
let werte = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let avg = moving_average(&werte, 3);
assert_eq!(avg, vec![2.0, 3.0, 4.0]); // (1+2+3)/3, (2+3+4)/3, (3+4+5)/3
}Der gleitende Durchschnitt (Moving Average) ist eine der häufigsten Operationen in Time-Series-Analyse und DSP. Statt einen Wert als Punkt zu bewerten, bewertest du ihn im Kontext seiner Nachbarn — das glättet Schwankungen und macht Trends sichtbarer.
Die Implementation mit windows(n) ist sehr direkt: jedes Fenster wird summiert und durch die Fenstergröße geteilt. Die Ausgabelänge ist daten.len() - n + 1, weil das erste Fenster mit Index 0 startet und das letzte mit Index daten.len() - n. Für 5 Eingangs-Werte und Fenstergröße 3 ergeben sich also 3 Ausgangs-Werte. Bei Hot-Loops mit großen Fenstern wäre eine Inkrementelle Variante (Summe pflegen, alten Wert subtrahieren, neuen addieren) deutlich schneller; die windows-Variante ist O(n*window_size) statt O(n).
Bigram-Analyse für Text (windows)
use std::collections::HashMap;
pub fn bigram_haeufigkeiten(woerter: &[&str]) -> HashMap<(String, String), u32> {
let mut counts = HashMap::new();
for paar in woerter.windows(2) {
let key = (paar[0].to_string(), paar[1].to_string());
*counts.entry(key).or_insert(0) += 1;
}
counts
}
fn main() {
let w = ["der", "schnelle", "braune", "fuchs", "der", "schnelle"];
let bigrams = bigram_haeufigkeiten(&w);
assert_eq!(bigrams.get(&("der".into(), "schnelle".into())), Some(&2));
}RGB-Pixel-Verarbeitung (chunks_exact)
pub fn invertieren_rgb(pixels: &mut [u8]) {
for pixel in pixels.chunks_exact_mut(3) {
pixel[0] = 255 - pixel[0];
pixel[1] = 255 - pixel[1];
pixel[2] = 255 - pixel[2];
}
// Falls Länge nicht durch 3 teilbar — Rest unverändert (Stand der iter).
}
fn main() {
let mut bild = vec![100u8, 150, 200, 50, 100, 150];
invertieren_rgb(&mut bild);
assert_eq!(bild, vec![155, 105, 55, 205, 155, 105]);
}RGB-Bildverarbeitung: jede Drei-Byte-Sequenz ist ein Pixel (Rot, Grün, Blau). chunks_exact_mut(3) iteriert in solchen Pixel-Schritten und gibt pro Iteration ein mutable Sub-Slice der Länge 3 zurück. Die Invertierung pro Kanal ist 255 - wert — einfach, aber für Millionen Pixel-Operationen muss es schnell sein.
Wegen der festen Block-Größe 3 kann der Compiler die Schleife oft auto-vektorisieren. Bei modernen CPUs werden mehrere Pixel pro Maschinen-Instruktion verarbeitet, was die Performance um Faktor 4 oder mehr erhöht. Mit chunks_mut(3) (ohne exact) wäre das schwieriger, weil der Compiler den Sonderfall „letzter kürzerer Block" mit einplanen müsste.
Audio-Frame-Verarbeitung (chunks)
pub fn frame_peaks(samples: &[i16], frame_size: usize) -> Vec<i16> {
samples.chunks(frame_size)
.map(|frame| frame.iter().map(|s| s.abs()).max().unwrap_or(0))
.collect()
}
fn main() {
let samples: Vec<i16> = (-5..=5).collect();
let peaks = frame_peaks(&samples, 3);
// Frames: [-5,-4,-3], [-2,-1,0], [1,2,3], [4,5]
assert_eq!(peaks, vec![5, 2, 3, 5]);
}Hash-Vergleich blockweise (chunks)
pub fn sind_blockgleich(a: &[u8], b: &[u8], block: usize) -> Option<usize> {
// Gibt den Index des ersten Blocks zurück, der NICHT gleich ist
for (i, (ba, bb)) in a.chunks(block).zip(b.chunks(block)).enumerate() {
if ba != bb {
return Some(i);
}
}
None
}Blockweiser Vergleich zweier Byte-Streams: nützlich, um den ersten unterschiedlichen Block zu finden, ohne den ganzen Stream zu vergleichen. Die chunks(block)-Iteratoren laufen synchron, zip paart sie elementweise, enumerate gibt den Block-Index dazu.
Anwendungen sind z. B. inkrementelle Datei-Vergleiche (welcher Block hat sich geändert?), Delta-Updates in Backup-Systemen, oder Diff-Algorithmen. Bei sehr großen Streams wäre eine Hash-basierte Variante (Rolling Hash pro Block) effizienter, aber die direkte Variante mit chunks + zip ist für viele Fälle ausreichend und sehr einfach zu lesen.
Sliding-Maximum (windows)
pub fn sliding_max(daten: &[i32], n: usize) -> Vec<i32> {
daten.windows(n)
.map(|w| *w.iter().max().unwrap())
.collect()
}
fn main() {
let v = vec![1, 3, 2, 5, 4, 1, 6];
assert_eq!(sliding_max(&v, 3), vec![3, 5, 5, 5, 6]);
}Triplet-Erkennung (windows + match)
pub fn finde_aufsteigend(daten: &[i32]) -> Option<usize> {
for (i, fenster) in daten.windows(3).enumerate() {
if let [a, b, c] = fenster {
if a < b && b < c {
return Some(i);
}
}
}
None
}
fn main() {
let v = [5, 3, 1, 2, 4, 7, 6];
assert_eq!(finde_aufsteigend(&v), Some(2)); // 1, 2, 4
}Eine sehr elegante Kombination von zwei Slice-Konzepten: windows(3) produziert alle Drei-Element-Fenster, und das innere if let [a, b, c] = fenster zerlegt jedes Fenster in seine drei Komponenten. Die Bedingung a < b && b < c prüft auf streng aufsteigende Triplets.
Diese Variante ist deutlich lesbarer als die Index-basierte Form (for i in 0..daten.len()-2 { if daten[i] < daten[i+1] && daten[i+1] < daten[i+2] { ... } }) und vermeidet Off-by-One-Fehler. Der enumerate gibt zusätzlich den Index zurück, sodass die Funktion die Start-Position des ersten passenden Triplets zurückgeben kann.
Daten-Partitionierung in feste Größen (chunks)
pub fn process_batches_u64(
daten: &[u64],
batch_size: usize,
mut handler: impl FnMut(&[u64]),
) {
for batch in daten.chunks(batch_size) {
handler(batch);
}
}
fn main() {
let user_ids: Vec<u64> = (1..=10).collect();
process_batches_u64(&user_ids, 3, |b| {
println!("Batch: {b:?}");
});
// Batch: [1, 2, 3]
// Batch: [4, 5, 6]
// Batch: [7, 8, 9]
// Batch: [10]
}Das Batch-Pattern ist überall dort wichtig, wo einzelne Operationen teuer sind und sich durch Bündelung amortisieren lassen — etwa Datenbank-Inserts (eine INSERT-Anweisung mit 1000 Rows ist schneller als 1000 einzelne), API-Calls (Bulk-Endpoints), Network-Sends (große Pakete statt vieler kleiner).
Die Funktion process_batches ist generisch über den Element-Typ und nimmt eine Callback-Closure. Der Aufrufer kann dann beliebige Logik pro Batch ausführen — etwa „sende diesen Batch an die API". chunks(batch_size) macht die Aufteilung; der letzte Batch ist möglicherweise kleiner. Wenn dein Endpoint nur volle Batches akzeptiert, würdest du stattdessen chunks_exact plus separate Behandlung des remainder nutzen.
Vector-Pair-Operation (chunks_exact)
pub fn skalar_produkt(a: &[f32], b: &[f32]) -> f32 {
a.chunks_exact(4)
.zip(b.chunks_exact(4))
.map(|(ca, cb)| {
ca[0]*cb[0] + ca[1]*cb[1] + ca[2]*cb[2] + ca[3]*cb[3]
})
.sum()
}Skalarprodukt ist eine zentrale numerische Operation in Linearer Algebra, Machine Learning, Signal-Verarbeitung. Die Implementation mit chunks_exact(4) und manueller Aufsummierung pro 4-Tupel ist nicht zufällig: sie führt den LLVM-Optimizer zu Auto-Vektorisierung. Moderne CPUs haben SIMD-Register (SSE, AVX), die 4 oder 8 f32-Werte gleichzeitig multiplizieren können — was die Operation um Faktor 4 oder 8 beschleunigt.
Bei sehr großen Vektoren wäre eine Crate wie wide oder packed_simd für noch explizitere Kontrolle interessant. Aber für viele Fälle reicht die chunks_exact-basierte Hand-Optimierung, die ohne externe Dependencies auskommt und vom Compiler erkannt wird.
Interessantes
windows(n) überlappt, chunks(n) nicht.
Das ist der zentrale Unterschied. windows für gleitende Pattern-Suche, chunks für disjunkte Block-Verarbeitung. Wer das durcheinander bringt, bekommt entweder zu viele oder zu wenige Iterationen.
chunks_exact ist Performance-freundlich.
Die feste Block-Größe macht es dem LLVM-Optimizer leicht, die Schleife zu vektorisieren (SIMD). In numerischem Code mit chunks_exact(4) oder chunks_exact(8) oft Faktor 2-4× schneller als chunks.
Es gibt kein windows_mut.
Überlappende mutable Slices würden die Aliasing-Regel brechen. Wer ähnliches braucht: über Indices iterieren mit split_at_mut-Tricks, oder die arrayvec/itertools-Crates für spezialisierte Adapter.
Bei kurzem Slice: windows liefert leere Iteration, chunks liefert einen kurzen Block.
[1, 2].windows(3) ergibt 0 Iterationen. [1, 2].chunks(3) ergibt 1 Iteration mit [1, 2]. Wer garantiert volle Blöcke braucht: chunks_exact plus remainder() für den Rest.
n == 0 panickt bei allen Varianten.
windows(0) und chunks(0) panicken zur Laufzeit. Bei dynamischem n vorher prüfen.
rchunks und rwindows für rückwärtige Iteration.
Iteration vom Slice-Ende her. Bei [1,2,3,4,5].rchunks(2) kommen [4,5], [2,3], [1]. Sehr nützlich bei Algorithmen, die natürlich am Ende beginnen.
chunks_exact-Iterator hat remainder().
Während des Iterierens ist der Rest noch verfügbar via iter.remainder(). Nach into_remainder() (verbrauchende Variante) bekommst du den Rest als &[T] für separate Behandlung.
windows kombiniert sich gut mit Slice-Patterns.
for win in daten.windows(3) { if let [a, b, c] = win { ... } } ist ein idiomatischer Weg, Triplets in einer Sequenz zu prüfen. Pattern-Matching plus Slice-Adapter — sehr elegant.
Weiterführende Ressourcen
Externe Quellen
- std::primitive.slice – windows
- std::primitive.slice – chunks
- std::primitive.slice – chunks_exact
- std::primitive.slice – rchunks
- The Rust Performance Book – SIMD