Slices sind eines der wichtigsten Konzepte in Rust — und gleichzeitig eines der ungewöhnlichsten für Programmierer aus C-, Java- oder Go-Welt. Ein Slice ist eine Referenz auf einen zusammenhängenden Speicherbereich, intern dargestellt als Fat Pointer: ein Pointer auf das erste Element und eine Längen-Information. Slices besitzen die Daten nicht — sie sehen sie nur. Genau das macht sie zum idiomatischen Funktions-Parameter für Sequenzen: eine Funktion mit &[i32] akzeptiert Arrays, Vecs und Sub-Bereiche all dieser, ohne dass du dich für eine konkrete Form entscheiden musst. Dieser Artikel ist ein Primer; Iteratoren und tieferes Slice-Pattern-Matching folgen im Slices-Kapitel.

Was ein Slice ist

Ein Slice ist immer als Referenz vorhanden: &[T] (immutable) oder &mut [T] (mutable). Der „nackte" Typ [T] allein existiert nur theoretisch — er ist ein dynamically-sized type (DST), den du nicht direkt als Wert auf dem Stack halten kannst.

Rust Slices entstehen aus Arrays oder Vec
fn main() {
    let arr = [1, 2, 3, 4, 5];
    let v = vec![10, 20, 30, 40, 50];

    let s1: &[i32] = &arr;           // Slice über das ganze Array
    let s2: &[i32] = &arr[1..4];     // Slice über Teilbereich: [2, 3, 4]
    let s3: &[i32] = &v;              // Slice über Vec
    let s4: &[i32] = &v[..3];         // Erste 3 Elemente: [10, 20, 30]

    println!("{:?}", s2);
    println!("{:?}", s4);
}

Slice-Syntax

SyntaxBedeutung
&arr[..]Ganzes Array/Vec als Slice
&arr[start..]Ab Index start bis Ende
&arr[..end]Vom Anfang bis Index end (exklusiv)
&arr[start..end]Von start (inklusiv) bis end (exklusiv)
&arr[start..=end]Von start (inklusiv) bis end (inklusiv)

Out-of-Bounds in Slice-Bereichen panickt zur Laufzeit. Negative Indices gibt es nicht — usize ist unsigned.

Fat Pointer — wie Slices im Speicher aussehen

Eine &[T]-Referenz ist intern ein Paar aus zwei Werten:

  • Datenzeiger — Adresse des ersten Elements im zugrundeliegenden Speicher.
  • Länge — Anzahl der Elemente, als usize.
Rust Fat Pointer
use std::mem::size_of;

// Normale Referenz auf Array: ein Pointer (8 Bytes auf 64-bit)
println!("{}", size_of::<&[i32; 5]>());      // 8

// Slice-Referenz: Pointer + Länge (16 Bytes)
println!("{}", size_of::<&[i32]>());         // 16

Das ist der Grund, warum Slice-Funktionen über beliebig lange Sequenzen arbeiten können: die Länge ist Teil der Referenz, nicht Teil des Typs.

Dasselbe gilt für &str — ein String-Slice ist intern ein Fat Pointer auf UTF-8-Bytes plus Länge.

Slices als Funktions-Parameter

Das wichtigste Pattern: Funktionen, die mit Sequenzen arbeiten, sollten Slices nehmen, keine konkreten Container-Typen.

Rust Idiomatische Funktions-Parameter
// ✓ Idiomatisch — funktioniert mit Array, Vec, Sub-Bereich
fn summe(slice: &[i32]) -> i32 {
    slice.iter().sum()
}

// ✗ Unnötig eingeschränkt
fn summe_array(arr: [i32; 5]) -> i32 { /* nur 5-Element-Arrays */ }
fn summe_vec(v: &Vec<i32>) -> i32 { /* nur Vecs */ }

fn main() {
    let arr = [1, 2, 3];
    let v = vec![1, 2, 3, 4, 5];

    println!("{}", summe(&arr));         // 6
    println!("{}", summe(&v));           // 15
    println!("{}", summe(&v[1..4]));     // 9
}

Der Aufrufer entscheidet, was er hat — die Funktion akzeptiert es transparent über Deref-Coercion:

  • &arr (vom Typ &[i32; 5]) wird automatisch zu &[i32].
  • &v (vom Typ &Vec<i32>) wird automatisch zu &[i32].
  • &v[1..4] ist bereits &[i32].

Mutable Slices

&mut [T] ist ein Slice mit Schreibzugriff:

Rust Mutable Slice
fn verdopple(slice: &mut [i32]) {
    for x in slice {
        *x *= 2;
    }
}

fn main() {
    let mut arr = [1, 2, 3];
    verdopple(&mut arr);
    println!("{:?}", arr);     // [2, 4, 6]
}

Wichtig: Borrow-Regeln gelten — du kannst kein &mut [T] und gleichzeitig ein &[T] auf den gleichen Bereich haben. Der Borrow Checker prüft das.

Slice-Methoden

Die Standard-Bibliothek hat eine sehr umfangreiche API auf Slices. Hier ein Auszug:

Rust Lesen
let s = [10, 20, 30, 40, 50];

s.len();                         // 5
s.is_empty();                    // false
s.first();                       // Some(&10)
s.last();                        // Some(&50)
s.get(2);                        // Some(&30)
s.get(10);                        // None — sicher

s.contains(&30);                  // true
s.iter().position(|&x| x > 25);   // Some(2)
s.iter().max();                   // Some(&50)
s.iter().sum::<i32>();            // 150

let pos = s.binary_search(&30);   // Ok(2) — funktioniert nur auf sortierten
Rust Mutieren
let mut s = [3, 1, 4, 1, 5, 9, 2, 6];

s.sort();                         // In-place sortieren
s.reverse();                      // In-place umkehren
s.swap(0, 1);                     // Zwei Elemente tauschen
s.fill(0);                        // Alle auf 0 setzen
Rust Aufteilen
let s = [1, 2, 3, 4, 5, 6];

let (links, rechts) = s.split_at(3);
// links = [1, 2, 3], rechts = [4, 5, 6]

for fenster in s.windows(3) {
    println!("{:?}", fenster);
    // [1,2,3], [2,3,4], [3,4,5], [4,5,6]
}

for paar in s.chunks(2) {
    println!("{:?}", paar);
    // [1,2], [3,4], [5,6]
}

windows(n) liefert überlappende Fenster, chunks(n) nicht-überlappende Stücke. Beide sind häufig in Algorithmen und Stream-Verarbeitung.

&str — der spezielle String-Slice

&str (ausgesprochen „string slice") ist ein Slice über UTF-8-Bytes. Strukturell ist es ein Fat Pointer auf den ersten Byte plus Länge in Bytes:

Rust &str ist ein Slice
let s: &str = "Hallo";
println!("len(): {}", s.len());      // 5 — Bytes
println!("bytes: {:?}", s.as_bytes());   // [72, 97, 108, 108, 111]

let teil: &str = &s[0..3];           // "Hal"
println!("{teil}");

Wichtig: Bei Multi-Byte-Zeichen (UTF-8) muss die Slice-Grenze an einer Zeichen-Grenze liegen, sonst gibt's Panic:

Rust String-Slice mit Multi-Byte
let s = "Hü";              // 'H' = 1 Byte, 'ü' = 2 Bytes
// let bad = &s[0..1];      // ok, "H"
// let panic = &s[0..2];    // Panic — Grenze mitten in 'ü'

Mehr Details und sichere Iteration im Strings-Kapitel.

Slice-Iteration

Slices implementieren IntoIteratorfor-Loops, iter, iter_mut, into_iter:

Rust Iteration über Slice
let v = vec![10, 20, 30];

// Über Referenzen (Slice-iter ist by-ref)
for &x in &v {
    println!("{x}");
}

// Mit Index
for (i, &x) in v.iter().enumerate() {
    println!("{i}: {x}");
}

// Mutable Iteration
let mut v = vec![1, 2, 3];
for x in v.iter_mut() {
    *x *= 10;
}
println!("{:?}", v);          // [10, 20, 30]

for &x in &v zieht einen Slice (&Vec<T>&[T]) und iteriert über Referenzen — das & im Pattern destrukturiert die Referenz, sodass x der Wert ist (nur möglich, wenn T: Copy).

Slices und Lifetimes (Teaser)

Ein Slice ist eine Referenz — er hat eine Lifetime:

Rust Slice mit Lifetime
fn ersten_drei(slice: &[i32]) -> &[i32] {
    &slice[..3]
}

fn main() {
    let arr = [1, 2, 3, 4, 5];
    let teil = ersten_drei(&arr);
    println!("{:?}", teil);
}

Die Lifetime des zurückgegebenen Slices ist (implizit) an die Lifetime des Eingabe-Slices gebunden — teil darf nicht länger leben als arr. Der Compiler verfolgt das automatisch.

Im Lifetimes-Kapitel wird das explizit gemacht und ausführlich behandelt.

Praxis: Slices in echtem Code

Network-Buffer verarbeiten

Beim Lesen aus einem Socket füllt read() einen Buffer — und liefert die Anzahl tatsächlich gelesener Bytes. Der wirksame Inhalt ist ein Sub-Slice.

Rust TCP-Read
use std::io::Read;
use std::net::TcpStream;

fn lese_header(stream: &mut TcpStream) -> std::io::Result<Vec<u8>> {
    let mut buffer = [0u8; 4096];
    let gelesen = stream.read(&mut buffer)?;       // gelesen <= 4096

    // Nur der wirklich gefüllte Teil — kein Heap-Alloc, kein Copy
    let inhalt: &[u8] = &buffer[..gelesen];

    // Suche das erste \r\n\r\n (Header-Ende)
    if let Some(pos) = inhalt.windows(4).position(|w| w == b"\r\n\r\n") {
        Ok(inhalt[..pos].to_vec())
    } else {
        Ok(inhalt.to_vec())
    }
}

windows(4) ist ideal für Pattern-Suche in einem Byte-Stream — gleitet ein 4-Byte-Fenster über den Slice, sucht nach \r\n\r\n als HTTP-Header-Terminator.

CSV-Zeile parsen

Ein Slice von Bytes in Felder zerlegen, ohne den ursprünglichen Speicher zu kopieren:

Rust CSV-Split
fn parse_csv_zeile(zeile: &str) -> Vec<&str> {
    zeile.split(',').map(str::trim).collect()
}

fn main() {
    let raw = " 42, Berlin , 2026-05-18 ";
    let felder = parse_csv_zeile(raw);
    assert_eq!(felder, vec!["42", "Berlin", "2026-05-18"]);
}

split liefert einen Iterator über &str-Slices in die Originalzeile — keine Kopien, keine Allokationen für die einzelnen Felder. Erst collect() packt sie in einen Vec.

Bildmanipulation: Pixel-Slice

Ein Bild als [u8] mit 3 Bytes pro Pixel (RGB) — chunks_exact(3) iteriert pixelweise:

Rust Bild-Helligkeit
fn durchschnitts_helligkeit(rgb: &[u8]) -> f32 {
    assert_eq!(rgb.len() % 3, 0);
    let pixel_anzahl = (rgb.len() / 3) as f32;

    let summe: u64 = rgb.chunks_exact(3)
        .map(|p| {
            // Luma-Formel: 0.3R + 0.59G + 0.11B
            (p[0] as u64 * 30 + p[1] as u64 * 59 + p[2] as u64 * 11) / 100
        })
        .sum();

    summe as f32 / pixel_anzahl
}

chunks_exact ist chunks ohne Rest-Pixel am Ende — wenn die Länge ein Vielfaches von 3 ist, identisch; sonst werden Rest-Bytes ignoriert (sicherer für ausgerichtete Buffer).

Binäre Suche in sortierten Daten

Rust Lookup
fn finde_user_id(sortierte_ids: &[u64], gesucht: u64) -> Option<usize> {
    sortierte_ids.binary_search(&gesucht).ok()
}

fn main() {
    let ids = [101, 142, 250, 384, 501, 642];
    assert_eq!(finde_user_id(&ids, 250), Some(2));
    assert_eq!(finde_user_id(&ids, 999), None);
}

binary_search ist O(log n) und arbeitet auf jedem Slice — ohne dass die Daten einem speziellen Container angehören müssen. Die Funktion akzeptiert einen Vec<u64>, ein [u64; N] oder einen Sub-Slice, alles über &[u64].

Interessantes

[T] ohne Referenz existiert nur theoretisch.

Der Typ [T] ist „unsized" — der Compiler kennt seine Größe nicht. Du kannst keine Variable vom Typ [T] haben, kein Funktions-Parameter arr: [T]. Was du immer hast: eine Referenz &[T], &mut [T], oder ein Box<[T]>. Diese drei Konstrukte fügen die Längen-Information am Pointer hinzu.

Vec und [T; N] derefen beide zu [T].

Das ist Magie der Deref-Coercion. Vec<T> implementiert Deref<Target = [T]>, Arrays haben eine special-case-Coercion. Resultat: Funktionen mit &[T]-Parameter funktionieren mit beiden — der eine Hauptgrund, warum man fast nie &Vec<T> als Parameter schreibt, sondern immer &[T].

Slice-Bereiche mit .. sind exklusiv am Ende.

&v[1..4] enthält die Indices 1, 2, 3 — nicht 4. Das ist die Standard-Konvention in Rust für Ranges. Wer den End-Index inklusiv will: &v[1..=4] (inklusive) liefert Indices 1, 2, 3, 4.

Box<[T]> ist ein Heap-allozierter Slice.

Ein Slice mit Ownership statt Borrowing. Beispiel: aus vec.into_boxed_slice(). Vorteil gegenüber Vec<T>: keine Capacity-Tracking, kleiner Memory-Overhead. Nachteil: kann nicht wachsen. Für immutable Daten oft die bessere Wahl.

split_at mit Index außerhalb der Grenzen panickt.

Sicherer: split_at_checked (seit Rust 1.80), gibt Option<(&[T], &[T])> zurück. Manche Slice-Methoden haben _checked-Varianten — ein Blick in die Stdlib-Doku lohnt sich für Fehler-resistente Code-Pfade.

iter().collect() kann ein Slice nicht erzeugen.

collect braucht einen Container-Typ, der FromIterator implementiert. [T; N] kann das nicht (Länge zur Compile-Zeit fest), [T] ist unsized. Du kannst nur in Vec<T> (oder ähnliche) collecten und ggf. ein Slice davon nehmen.

Slice-Pattern-Matching ist sehr ausdrucksstark.

match slice { [first, .., last] => ..., [single] => ..., [] => ... } — Patterns mit .. matchen einen Bereich beliebiger Länge. Sehr nützlich für Algorithmen auf Slices. Mehr im Pattern-Matching-Artikel.

Slice-Konvertierung von &Vec zu &[T] ist „kostenlos“.

Ein &Vec<T> ist ein Pointer auf die Vec-Struktur (Pointer + Länge + Capacity). Die Deref-Coercion baut daraus einen Fat Pointer (Pointer + Länge) — keine Daten-Kopie, kein Heap-Touch. Im Maschinencode oft nur ein Register-Lade-Vorgang.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Primitive Datentypen

Zur Übersicht