Slice-Patterns sind eine der elegantesten Formen des Rust-Pattern-Matching: du kannst direkt auf den Inhalt und die Länge eines Slices matchen, einzelne Elemente an Namen binden und „Rest"-Bereiche mit .. ausblenden oder festhalten. Damit lassen sich Algorithmen, die in anderen Sprachen mit Index-Arithmetik und Längen-Checks erstickt würden, sehr kompakt schreiben. Dieser Artikel zeigt die volle Syntax — von einfachen Längen-Checks bis zu Rest-Bindings mit @-Operator — und beweist, dass Slice-Patterns nicht nur schön, sondern auch praktisch sind.

Die Grundformen

Exakte Länge

Rust Fixed-Length
fn beschreibe(s: &[i32]) -> String {
    match s {
        [] => String::from("leer"),
        [_] => String::from("ein Element"),
        [_, _] => String::from("zwei Elemente"),
        [_, _, _] => String::from("drei Elemente"),
        _ => String::from("mehr als drei"),
    }
}

fn main() {
    assert_eq!(beschreibe(&[]), "leer");
    assert_eq!(beschreibe(&[1]), "ein Element");
    assert_eq!(beschreibe(&[1, 2, 3]), "drei Elemente");
    assert_eq!(beschreibe(&[1, 2, 3, 4]), "mehr als drei");
}

Hier siehst du die einfachste Form von Slice-Patterns: jedes _ matcht genau ein Element, und die Anzahl der _ im Pattern bestimmt die geforderte Länge. [_, _, _] matcht damit Slices mit exakt drei Elementen — kein mehr, kein weniger.

Diese Patterns kombinieren elegant zwei Prüfungen, die du sonst getrennt schreiben müsstest: die Längen-Validierung (s.len() == 3) und die strukturelle Form. Bei match läuft jeder Arm in Reihenfolge — der Compiler stellt zudem Exhaustiveness sicher, das heißt: jeder mögliche Input-Slice muss von einem Arm getroffen werden, sonst Compile-Fehler.

Elemente binden

Rust Bindings
fn extrahiere_drei(s: &[i32]) -> Option<(i32, i32, i32)> {
    match s {
        [a, b, c] => Some((*a, *b, *c)),
        _ => None,
    }
}

fn main() {
    assert_eq!(extrahiere_drei(&[10, 20, 30]), Some((10, 20, 30)));
    assert_eq!(extrahiere_drei(&[1, 2]), None);
}

Statt _ als Wildcards kannst du Namen in das Pattern schreiben — dann werden die jeweiligen Elemente an diese Namen gebunden. Im Beispiel werden a, b, c an die drei Elemente eines passenden Slices gebunden und stehen im Match-Arm zur Verfügung.

Eine wichtige Subtilität: bei &[i32] (Slice von i32-Referenzen) werden die Bindings als &i32 gebunden, nicht als i32 direkt. Beim Ergebnis-Tupel müssen wir mit *a, *b, *c dereferenzieren, um die Werte rauszuziehen. Bei Copy-Typen wie i32 greift im Tupel-Konstruktor zwar Auto-Deref, aber für klare Lesbarkeit ist die explizite Dereferenzierung üblich.

Der ..-Rest-Operator

Mit .. kannst du beliebig viele Elemente überspringen:

Rust Rest-Pattern
fn first_und_last(s: &[i32]) -> Option<(i32, i32)> {
    match s {
        [first, .., last] => Some((*first, *last)),
        [only] => Some((*only, *only)),
        [] => None,
    }
}

fn main() {
    assert_eq!(first_und_last(&[1, 2, 3, 4]), Some((1, 4)));
    assert_eq!(first_und_last(&[42]), Some((42, 42)));
    assert_eq!(first_und_last(&[]), None);
}

Der ..-Operator ist das mächtigste Werkzeug in Slice-Patterns: er steht für „beliebig viele Elemente an dieser Stelle". [first, .., last] matcht jeden Slice mit mindestens zwei Elementen — erstes und letztes werden gebunden, alles dazwischen wird ignoriert. Das .. kann 0 oder mehr Elemente abdecken; bei einem 2-elementigen Slice ist es leer, bei einem 100-elementigen sind es 98 Elemente.

Im Beispiel siehst du eine schöne Anwendung: die Match-Arme sind nach abnehmender Spezifität geordnet. [first, .., last] greift zuerst (mindestens 2 Elemente), dann [only] für 1-elementige Slices, schließlich [] für den leeren Fall. So entsteht eine kompakte, exhaustive Funktion ohne Index-Arithmetik.

Rest binden mit @

Wenn du den Rest auch als Slice brauchst, nutzt du den @-Operator:

Rust Rest binden
fn kopf_und_schwanz(s: &[i32]) -> Option<(i32, &[i32])> {
    match s {
        [head, tail @ ..] => Some((*head, tail)),
        [] => None,
    }
}

fn main() {
    assert_eq!(kopf_und_schwanz(&[1, 2, 3]), Some((1, &[2, 3][..])));
    assert_eq!(kopf_und_schwanz(&[]), None);
}

Wenn du nicht nur einzelne Elemente, sondern auch den Rest-Slice weiterverarbeiten willst, nutzt du den @-Operator: tail @ .. bindet den ..-Bereich als &[T]-Slice an den Namen tail. Damit kannst du klassische funktionale Algorithmen direkt schreiben — Kopf abtrennen, Rest verarbeiten, rekurrieren.

Die Form [head, tail @ ..] ist das Rust-Pendant zu Haskells (head:tail)-Pattern. Sie matcht alle nicht-leeren Slices, bindet das erste Element an head und alles Übrige an tail. Bei einem 1-elementigen Slice ist tail ein leerer Slice — semantisch korrekt.

Front, Middle, Back

Rust Drei-Wege-Split
fn zerlege(s: &[i32]) -> Option<(i32, &[i32], i32)> {
    match s {
        [first, middle @ .., last] => Some((*first, middle, *last)),
        _ => None,
    }
}

fn main() {
    assert_eq!(zerlege(&[1, 2, 3, 4]), Some((1, &[2, 3][..], 4)));
    assert_eq!(zerlege(&[1, 2]), Some((1, &[][..], 2)));      // middle leer
    assert_eq!(zerlege(&[1]), None);                            // < 2 Elemente
}

[first, middle @ .., last] — erstes Element, mittleres Slice, letztes Element. Bei genau 2 Elementen ist middle ein leerer Slice.

Literale matchen

Du kannst auch konkrete Werte matchen:

Rust Literal-Match
fn ist_palindrom_kurz(s: &[i32]) -> bool {
    match s {
        [] => true,
        [_] => true,
        [a, b] => a == b,
        [a, _, c] => a == c,
        [a, b, c, d] => a == d && b == c,
        _ => false,        // länger: hier nicht behandelt
    }
}

Patterns mit Bindings — und auf den gebundenen Werten kann mit Guards weiter geprüft werden.

Konkrete Werte

Rust Mit Literalen
fn klassifiziere(rgb: &[u8]) -> String {
    match rgb {
        [0, 0, 0] => String::from("schwarz"),
        [255, 255, 255] => String::from("weiß"),
        [r, 0, 0] if *r > 0 => String::from("rot"),
        [0, g, 0] if *g > 0 => String::from("grün"),
        [0, 0, b] if *b > 0 => String::from("blau"),
        _ => String::from("mischfarbe"),
    }
}

fn main() {
    assert_eq!(klassifiziere(&[0, 0, 0]), "schwarz");
    assert_eq!(klassifiziere(&[255, 0, 0]), "rot");
    assert_eq!(klassifiziere(&[100, 50, 200]), "mischfarbe");
}

Konkrete Werte (0, 255) plus Guards (if *r > 0) ermöglichen sehr ausdrucksstarke Patterns.

Slice-Patterns in let

Slice-Patterns funktionieren auch in let-Bindungen und let else:

Rust let Slice-Pattern
fn main() {
    let punkte = [(1, 2), (3, 4), (5, 6)];
    let [erste, zweite, dritte] = punkte;
    println!("{erste:?} {zweite:?} {dritte:?}");
}

let [a, b, c] = ... funktioniert nur bei Arrays mit fester Länge, die zur Compile-Zeit bekannt ist. Bei dynamischen Slices: let else:

Rust let else mit Slice
fn parse_befehl(args: &[String]) -> Result<(&str, &str), String> {
    let [_program, cmd, arg] = args else {
        return Err(String::from("Aufruf: programm <cmd> <arg>"));
    };
    Ok((cmd, arg))
}

let [_program, cmd, arg] = args else { ... } matcht nur, wenn args exakt 3 Elemente hat. Sonst: divergierender Branch.

Rest in let else

Rust let mit Rest
fn erstes_argument(args: &[String]) -> Option<&str> {
    let [_program, erstes, ..] = args else {
        return None;
    };
    Some(erstes)
}

Sehr saubere CLI-Argument-Validierung.

Slice-Patterns auf String-Bytes

Patterns funktionieren auf jedem Slice-Typ — auch auf Byte-Slices:

Rust Magic-Bytes
fn erkenne_format(bytes: &[u8]) -> String {
    match bytes {
        [0x89, b'P', b'N', b'G', ..] => String::from("PNG"),
        [0xFF, 0xD8, 0xFF, ..] => String::from("JPEG"),
        [b'G', b'I', b'F', b'8', _, b'a', ..] => String::from("GIF"),
        [0x25, 0x50, 0x44, 0x46, ..] => String::from("PDF"),
        _ => String::from("unbekannt"),
    }
}

fn main() {
    assert_eq!(erkenne_format(&[0x89, b'P', b'N', b'G', 0x00, 0x00]), "PNG");
    assert_eq!(erkenne_format(&[0xFF, 0xD8, 0xFF, 0xE0]), "JPEG");
    assert_eq!(erkenne_format(b"GIF89a"), "GIF");
}

Dateiformat-Erkennung mit Magic-Bytes — extrem kompakt und lesbar. b'P' ist ein Byte-Literal vom Typ u8.

Slice-Patterns in if let und while let

Rust if let
fn main() {
    let daten: &[i32] = &[1, 2, 3, 4, 5];
    if let [first, .., last] = daten {
        println!("first={first}, last={last}");
    }
}
Rust while let — Slice abarbeiten
fn main() {
    let mut daten: &[i32] = &[1, 2, 3, 4, 5];
    while let [erstes, rest @ ..] = daten {
        println!("Bearbeite: {erstes}");
        daten = rest;
    }
}
// Ausgabe: 1, 2, 3, 4, 5

Sehr funktional — abarbeiten Element für Element ohne Index, mit rest @ .. als Restslice.

Praxis: Slice-Patterns im echten Code

IP-Adresse klassifizieren

Rust IPv4-Klassen
pub fn ipv4_klasse(addr: &[u8; 4]) -> String {
    match addr {
        [10, ..] => String::from("private (10.x.x.x)"),
        [172, b, ..] if (16..=31).contains(b) => String::from("private (172.16-31.x.x)"),
        [192, 168, ..] => String::from("private (192.168.x.x)"),
        [127, ..] => String::from("loopback"),
        [255, 255, 255, 255] => String::from("broadcast"),
        _ => String::from("public"),
    }
}

fn main() {
    assert_eq!(ipv4_klasse(&[192, 168, 1, 1]), "private (192.168.x.x)");
    assert_eq!(ipv4_klasse(&[127, 0, 0, 1]), "loopback");
    assert_eq!(ipv4_klasse(&[8, 8, 8, 8]), "public");
}

IPv4-Adressen sind 4-Byte-Arrays, und die Klassifikation lässt sich elegant mit Slice-Patterns ausdrücken. [10, ..] matcht alle Adressen, die mit 10 beginnen (das private Klasse-A-Netz), unabhängig von den restlichen drei Bytes. [172, b, ..] if (16..=31).contains(b) kombiniert Pattern und Guard: das zweite Byte wird gebunden und mit einer Range-Prüfung gegen das private 172er-Netz validiert.

Diese Form ist deutlich lesbarer als die äquivalente Bit-Maskierungs-Variante (addr & 0xFF000000 == 0x0A000000). Sie ist auch sicherer — der Compiler prüft, dass alle Fälle exhaustive abgedeckt sind, und du kannst nicht versehentlich falsche Masken oder Shift-Werte einbauen.

CLI-Argument-Dispatcher

Rust Argv-Pattern
pub fn dispatch(args: &[String]) -> Result<(), String> {
    match args {
        [_, cmd] if cmd == "help" => {
            println!("Verfügbare Befehle: help, status, restart");
            Ok(())
        }
        [_, cmd] if cmd == "status" => {
            println!("OK");
            Ok(())
        }
        [_, cmd, arg] if cmd == "restart" => {
            println!("Restart von {arg}");
            Ok(())
        }
        [_, cmd, ..] => Err(format!("unbekanntes Kommando: {cmd}")),
        _ => Err("kein Kommando angegeben".into()),
    }
}

CLI-Argument-Dispatch ist ein klassisches Anwendungsgebiet für Slice-Patterns. Statt mit argc-Checks und manuellen Index-Zugriffen zu hantieren, schreibst du direkt das gewünschte Argument-Layout als Pattern. Jeder Match-Arm prüft sowohl die Anzahl der Argumente als auch (über den Guard) den Kommando-Namen.

[_, cmd] matcht „Programm + ein Argument", [_, cmd, arg] matcht „Programm + zwei Argumente". Der erste Slot ist immer _, weil das traditionsgemäß der Programm-Name ist (argv[0]). Diese Variante ist gut für einfache CLIs; für komplexere Tools (mit Flags, Optionen, Subcommands) lohnen sich Crates wie clap.

Polynom-Auswertung

Rust Horner-Schema
// Horner-Schema rekursiv mit Slice-Patterns
pub fn polynom_eval(koeffizienten: &[f64], x: f64) -> f64 {
    match koeffizienten {
        [] => 0.0,
        [a] => *a,
        [a, rest @ ..] => a + x * polynom_eval(rest, x),
    }
}

fn main() {
    // 1 + 2*x + 3*x²
    let koef = [1.0, 2.0, 3.0];
    assert_eq!(polynom_eval(&koef, 2.0), 1.0 + 4.0 + 12.0);
}

Das Horner-Schema ist eine effiziente Methode, ein Polynom auszuwerten. Die rekursive Form mit Slice-Patterns ist besonders elegant: drei Match-Arme decken alle Fälle ab. Leerer Slice → Polynom hat keinen Wert (0). Ein-elementiger Slice → Konstante. Mehr-elementiger Slice → Koeffizient plus x mal dem Rest-Polynom.

Diese Variante ist nicht die effizienteste — der rekursive Aufruf baut Stack-Frames auf, was bei sehr hohen Polynomen problematisch werden könnte. Die iterative Variante (koeffizienten.iter().rev().fold(0.0, |acc, &k| acc * x + k)) ist schneller. Aber die rekursive Variante ist als Lehrbeispiel für Slice-Patterns extrem klar.

Versions-String-Parse

Rust Semver
pub fn parse_version(v: &str) -> Option<(u32, u32, u32)> {
    let parts: Vec<&str> = v.split('.').collect();
    match parts.as_slice() {
        [major, minor, patch] => Some((
            major.parse().ok()?,
            minor.parse().ok()?,
            patch.parse().ok()?,
        )),
        _ => None,
    }
}

fn main() {
    assert_eq!(parse_version("1.78.0"), Some((1, 78, 0)));
    assert_eq!(parse_version("1.78"), None);
    assert_eq!(parse_version("1.78.0.0"), None);
}

Direkt auf einem Vec lässt sich kein Slice-Pattern matchen — das Pattern braucht eine Slice-Referenz. parts.as_slice() konvertiert den Vec in einen &[&str], und auf diesem ist das Pattern-Matching gültig.

Das Pattern [major, minor, patch] macht zwei Dinge gleichzeitig: es validiert, dass exakt drei Teile vorliegen, und es bindet sie an Namen. Bei einer Eingabe wie "1.78" (nur zwei Teile) oder "1.78.0.0" (vier Teile) greift der _-Arm und liefert None. Diese saubere Trennung von Längen-Prüfung und Wert-Extraktion ist genau die Stärke der Slice-Patterns.

Tail-Recursive Listen-Verarbeitung

Rust Rekursive Summe
pub fn summe_rekursiv(slice: &[i32]) -> i32 {
    match slice {
        [] => 0,
        [first, rest @ ..] => *first + summe_rekursiv(rest),
    }
}

fn main() {
    assert_eq!(summe_rekursiv(&[1, 2, 3, 4, 5]), 15);
}

Funktionaler Stil — Basis-Case (leerer Slice) + Rekursions-Case (erstes Element plus Rest). Klassische Funktional-Pattern.

Min-Max-Track durch rekursives Splitting

Rust Min-Max
pub fn min_max(slice: &[i32]) -> Option<(i32, i32)> {
    match slice {
        [] => None,
        [only] => Some((*only, *only)),
        [first, rest @ ..] => {
            let (sub_min, sub_max) = min_max(rest)?;
            Some((first.min(&sub_min).clone(), first.max(&sub_max).clone()))
        }
    }
}

fn main() {
    assert_eq!(min_max(&[3, 1, 4, 1, 5, 9, 2, 6]), Some((1, 9)));
}

Erste-und-Letzte-Element-Validation

Rust Validate Endpoints
pub fn ist_pfad_absolut(parts: &[&str]) -> bool {
    match parts {
        [first, ..] if first.is_empty() => true,    // beginnt mit `/`
        _ => false,
    }
}

fn main() {
    let absolute = "/etc/hosts".split('/').collect::<Vec<_>>();
    let relative = "etc/hosts".split('/').collect::<Vec<_>>();
    assert!(ist_pfad_absolut(&absolute));
    assert!(!ist_pfad_absolut(&relative));
}

Packet-Header-Validierung

Rust Header-Validator
pub fn validiere_paket(bytes: &[u8]) -> Result<(), String> {
    match bytes {
        [] => Err(String::from("leeres Paket")),
        [magic, ..] if *magic != 0xAB => Err(String::from("falsches Magic-Byte")),
        [_, version, ..] if *version != 1 => Err(String::from("unbekannte Version")),
        [_, _, len, .., payload @ ..] if *len as usize != payload.len() => {
            Err(String::from("Längen-Feld falsch"))
        }
        _ => Ok(()),
    }
}

Multi-Stufen-Validierung in einem match. Jeder Arm prüft eine andere Bedingung.

Besonderheiten

Slice-Patterns prüfen Länge UND Inhalt in einem Schritt.

Anstatt if v.len() == 3 && v[0] == 1 { ... } schreibst du if let [1, _, _] = v.as_slice() { ... }. Kompakter, ohne Index-Out-of-Bounds-Risiko, und Pattern-Matching ist exhaustive geprüft.

.. ist Rest-Pattern, name @ .. bindet ihn.

[a, .., b] ignoriert den Rest. [a, mid @ .., b] bindet ihn als Slice. Wer den Rest weiterverarbeiten will, braucht das @. Wer ihn nur überspringen will, reicht ...

Patterns sind refutable in match, infallibel in normalem let.

let [a, b] = &slice; ist Compile-Fehler bei slice: &[i32] — die Länge ist nicht garantiert. In match, if let, while let, let else darfst du refutable Patterns nutzen. Bei festen Array-Längen (&[i32; 3]) ist auch direktes let ok.

Bei &[T] matchen Bindings auf &T.

match s { [a, b, c] => ... } mit s: &[i32] bindet a, b, c als &i32. Beim Vergleich oder Arithmetik brauchst du oft *a zum Dereferenzieren — außer bei Copy-Typen, wo Auto-Deref greift.

.. matcht null oder mehr Elemente.

[a, ..] matcht jeden Slice mit mindestens 1 Element. [a, b, ..] mindestens 2. [a, .., b] mindestens 2 (das .. darf 0 sein). [..] matcht jeden Slice, inklusive leer.

Slice-Patterns funktionieren auf Vec via as_slice.

match v.as_slice() { ... } mit v: Vec<T> macht Slice-Pattern-Matching auf den Vec-Inhalt möglich. Direkter Match auf Vec ist nicht erlaubt — der Slice-Pattern braucht eine Slice-Referenz.

Slice-Patterns sind exhaustive geprüft.

Bei einem match über &[i32] muss jeder mögliche Slice von einem Arm getroffen werden. Sonst Compile-Fehler. Mit _ als Wildcard-Arm sicher abgedeckt. Die Exhaustiveness-Garantie ist eine der Kern-Stärken von Pattern-Matching.

Guards (if ...) ergänzen Patterns.

Wenn das Pattern allein nicht reicht: Guard hinzufügen. [a, ..] if a.is_positive() => ... matcht nur, wenn das erste Element positiv ist. Sehr nützlich für semantische Bedingungen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Slices & Views

Zur Übersicht