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.
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
| Syntax | Bedeutung |
|---|---|
&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.
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]>()); // 16Das 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.
// ✓ 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:
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:
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 sortiertenlet 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 setzenlet 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:
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:
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 IntoIterator — for-Loops, iter, iter_mut, into_iter:
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:
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.
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:
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:
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
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
- The Rust Book – Slice Type
- Rust Reference – Slice types
- std::primitive.slice – Methoden-Doku
- std::slice – Iterator-Typen
- Rust by Example – Slices