&[T] ist der allgemeine Element-Slice — eine Referenz auf einen zusammenhängenden Bereich von T-Werten beliebiger Länge. Wo &str das Spezial-Format für UTF-8-Strings ist, ist &[T] die generische Form für jede andere Sequenz: &[i32], &[u8], &[Person], &[(K, V)]. Eine Funktion mit &[T]-Parameter akzeptiert Arrays, Vecs, Sub-Bereiche von beidem — über Auto-Deref-Coercion alles transparent. Dieser Artikel zeigt die Range-Syntax in voller Breite, das interne Speicher-Layout, die wichtigsten Methoden und macht klar, warum &[T] der Standard-Funktions-Parameter für Sequenzen ist.
Was &[T] ist
Genau wie &str ist &[T] ein Fat Pointer:
use std::mem::size_of;
fn main() {
let v: Vec<i32> = vec![10, 20, 30];
let s: &[i32] = &v;
println!("{}", size_of::<&[i32]>()); // 16 — Pointer + Länge
println!("{}", size_of::<&i32>()); // 8 — nur Pointer
println!("{}", s.len()); // 3
}Die zwei Felder eines Fat Pointers haben jeweils eine klare Aufgabe. Der Pointer zeigt auf das erste Element des Bereichs — es ist eine Adresse, also 8 Bytes auf 64-bit-Systemen. Die Länge gibt an, wie viele Elemente der Slice umfasst — in der Anzahl der Elemente, nicht in Bytes. Der Compiler kennt die Element-Größe statisch (über den generischen Parameter T), kann also die Byte-Größe bei Bedarf berechnen.
Wichtig zu verstehen: der Slice besitzt die Elemente nicht — er borrowt sie. Die Original-Sammlung (Array, Vec, oder ein anderer Heap-Block) bleibt der Besitzer. Wenn der Slice gedroppt wird, passiert nichts — die Elemente leben weiter, solange die Originalquelle existiert. Wenn die Originalquelle dagegen gedroppt würde, während noch ein Slice darauf zeigt, hätten wir eine Dangling Reference — und genau das verhindert der Borrow Checker durch Lifetime-Verfolgung.
Wie ein Slice entsteht
Auto-Deref-Coercion von Vec
let v: Vec<i32> = vec![1, 2, 3, 4, 5];
let s: &[i32] = &v; // &Vec<i32> → &[i32] via Deref
// Vec<T> implementiert Deref<Target = [T]>Auto-Deref-Coercion von Array
let a: [i32; 5] = [1, 2, 3, 4, 5];
let s: &[i32] = &a; // &[i32; 5] → &[i32]Range-Indexierung
let v = vec![10, 20, 30, 40, 50];
let s1: &[i32] = &v[..]; // ganzer Vec als Slice
let s2: &[i32] = &v[..3]; // [10, 20, 30]
let s3: &[i32] = &v[2..]; // [30, 40, 50]
let s4: &[i32] = &v[1..4]; // [20, 30, 40]
let s5: &[i32] = &v[1..=3]; // [20, 30, 40] (inklusiv)Die Range-Syntax [start..end] macht aus jeder Sammlung einen Sub-Slice — ohne Allokation, ohne Kopie, einfach durch Verschieben des Pointers und Anpassen der Länge. Im Speicher entsteht nichts Neues; der Sub-Slice zeigt auf denselben Heap-Bereich, nur mit anderem Start und anderer Länge.
Die Range-Syntax kennt fünf Varianten:
| Syntax | Bedeutung |
|---|---|
.. | Alles |
..n | Vom Anfang bis Index n (exklusiv) |
n.. | Ab Index n bis Ende |
n..m | Von n (inklusiv) bis m (exklusiv) |
n..=m | Von n bis m (beide inklusiv) |
Wer einen Range angibt, der über die Slice-Grenzen hinausgeht (z. B. &v[..10] bei einem 5-elementigen Vec), bekommt einen Runtime-Panic — Rust prüft die Range-Bounds zur Laufzeit. Wenn die Indices aus unsicherer Quelle stammen (Nutzer-Input, externe API), ist get(range) die sichere Alternative; sie liefert Option<&[T]> und panickt nicht.
Die Unterscheidung zwischen ..n (exklusiv) und ..=n (inklusiv) ist eine häufige Stolperfalle. &v[..3] liefert drei Elemente (Indices 0, 1, 2); &v[..=3] liefert vier Elemente (Indices 0, 1, 2, 3). Die exklusive Variante ist die häufigere und passt zur Rust-Konvention „end ist exklusiv" (wie in for i in 0..n).
Die wichtigsten Slice-Methoden
Längen und Prüfungen
let v = [10, 20, 30];
let s: &[i32] = &v;
s.len(); // 3
s.is_empty(); // false
s.first(); // Some(&10)
s.last(); // Some(&30)Sicherer Zugriff
let s = [10, 20, 30];
let a = s[1]; // 20 — Panic bei Out-of-Bounds
let b = s.get(1); // Some(&20)
let c = s.get(99); // None — kein Panic
let d = s.get(1..3); // Some(&[20, 30])Beim Zugriff auf Slice-Elemente bietet Rust zwei Wege, die sich in der Fehler-Semantik unterscheiden. Der direkte s[i] panickt bei Out-of-Bounds — er ist ergonomisch, aber unsicher bei Indices aus unsicherer Quelle. s.get(i) gibt Option<&T> zurück und ist die fallible Variante: bei gültigem Index Some(&wert), bei ungültigem None. Du musst dann mit if let oder match oder ? weiterverarbeiten.
Interessant ist auch s.get(1..3) — get akzeptiert nicht nur einzelne Indices, sondern auch Ranges, und liefert dann Option<&[T]>. Damit kannst du einen Sub-Slice auf Bounds-Sicherheit prüfen, ohne einen Range-Index zu riskieren. Sehr nützlich bei Protokoll-Parsern, wo die Buffergröße nicht garantiert ist.
Iteration
let s = [1, 2, 3, 4, 5];
for x in s.iter() { // &i32
print!("{x} ");
}
for (i, x) in s.iter().enumerate() { // (usize, &i32)
print!("{i}={x} ");
}
let summe: i32 = s.iter().sum(); // 15
let max = s.iter().max(); // Some(&5)Suche
let s = [10, 20, 30, 40, 50];
s.contains(&30); // true
s.iter().position(|&x| x > 25); // Some(2)
s.iter().find(|&&x| x > 25); // Some(&30)
s.iter().any(|&x| x > 100); // false
s.iter().all(|&x| x > 5); // true
// binary_search auf sortiertem Slice
let pos = s.binary_search(&30); // Ok(2)
let missing = s.binary_search(&25); // Err(2) — würde an Index 2 eingefügtDie Suchmethoden teilen sich grob in zwei Kategorien. Lineare Suche (contains, position, find, any, all) durchläuft den Slice von vorne und stoppt beim ersten Treffer — O(n) im Worst Case. Sie funktioniert auf jedem Slice, sortiert oder nicht.
Binäre Suche (binary_search) ist O(log n), funktioniert aber nur auf sortierten Slices. Bei einem 1-Million-Elemente-Slice ist binäre Suche etwa 50 000-mal schneller als lineare. Der Rückgabewert ist Result<usize, usize>: bei einem Treffer Ok(index), bei Misserfolg Err(insert_position) — also die Position, an der das gesuchte Element eingefügt werden müsste, um die Sortierung zu erhalten. Diese Err-Information ist oft genauso wertvoll wie der Ok-Fall — sie ermöglicht „insert in sortierte Liste" in O(log n).
Splitting
let s = [1, 2, 0, 3, 4, 0, 5];
// Bei jedem Vorkommen von 0 splitten
let teile: Vec<&[i32]> = s.split(|&x| x == 0).collect();
// [[1, 2], [3, 4], [5]]
// Erste Hälfte / zweite Hälfte
let (links, rechts) = s.split_at(3);
// links = [1, 2, 0], rechts = [3, 4, 0, 5]
// Erstes Element + Rest
if let Some((kopf, schwanz)) = s.split_first() {
println!("Kopf: {kopf}, Schwanz: {schwanz:?}");
}Konvertierungen
let s = [1, 2, 3];
let als_vec: Vec<i32> = s.to_vec(); // alloziert neu
let als_array: [i32; 3] = s.try_into().unwrap(); // wenn Länge passtAPI-Pattern: &[T] als Parameter
Wie bei &str gilt: &[T] ist die idiomatische Form für Sequenz-Parameter. Sie akzeptiert alles, was sich zu einem Slice coercen lässt.
pub fn summe(daten: &[i32]) -> i32 {
daten.iter().sum()
}
fn main() {
let array = [1, 2, 3, 4];
let vec = vec![10, 20, 30];
assert_eq!(summe(&array), 10);
assert_eq!(summe(&vec), 60);
assert_eq!(summe(&vec[1..]), 50);
assert_eq!(summe(&[]), 0); // leerer Slice
}Im Beispiel siehst du, wie eine einzige Funktion mit &[i32]-Parameter alle relevanten Aufrufer-Formen abdeckt: ein fixed-size Array ([1, 2, 3, 4]), ein Vec (vec![10, 20, 30]), ein Sub-Slice (&vec[1..]), sogar ein leerer Slice (&[]). Jede dieser Varianten wird zur Compile-Zeit zu derselben &[i32]-Repräsentation reduziert — Pointer plus Länge.
Diese Universalität ist der Grund, warum &[T] der idiomatische Funktions-Parameter für Sequenzen ist. Wer stattdessen &Vec<T> schreibt, schließt Arrays und Sub-Slices aus, ohne irgendetwas zu gewinnen — die Performance ist identisch. Clippy warnt mit clippy::ptr_arg bei &Vec<T>-Parametern, und die Warnung ist in fast allen Fällen berechtigt.
Slices sind Copy
Eine Slice-Referenz ist Copy (8/16-Byte-Pointer-Bytes):
fn main() {
let v = vec![1, 2, 3];
let s1: &[i32] = &v;
let s2 = s1; // Kopie der Referenz
let s3 = s1; // weitere Kopie — s1 weiterhin nutzbar
assert_eq!(s1.len() + s2.len() + s3.len(), 9);
}Mehrfache Verwendung in derselben Funktion ohne Borrow-Probleme.
Slices und Lifetimes
Ein Slice ist immer an die Lebenszeit seiner Quelle gebunden:
fn ersten_drei(daten: &[i32]) -> &[i32] {
&daten[..3.min(daten.len())]
}
// Implizit: fn ersten_drei<'a>(daten: &'a [i32]) -> &'a [i32]
fn main() {
let v = vec![1, 2, 3, 4, 5];
let teil = ersten_drei(&v);
println!("{teil:?}"); // [1, 2, 3]
}teil lebt nicht länger als v. Wenn v gedroppt würde, wäre teil ungültig — der Compiler verhindert das. Mehr im Lifetimes-Kapitel.
Praxis: &[T] im echten Code
Statistik-Library
pub fn mittel(werte: &[f64]) -> Option<f64> {
if werte.is_empty() { return None; }
let summe: f64 = werte.iter().sum();
Some(summe / werte.len() as f64)
}
pub fn varianz(werte: &[f64]) -> Option<f64> {
let m = mittel(werte)?;
let summe: f64 = werte.iter()
.map(|v| (v - m).powi(2))
.sum();
Some(summe / werte.len() as f64)
}
fn main() {
let messungen = vec![1.0, 2.0, 3.0, 4.0, 5.0];
assert_eq!(mittel(&messungen), Some(3.0));
assert_eq!(varianz(&messungen), Some(2.0));
}Eine klassische Statistik-Library — mittel und varianz mit &[f64]-Parameter. Beide Funktionen sind read-only und produzieren neue Werte als Rückgabe; das Original-Array bleibt unangetastet und kann beliebig oft an andere Berechnungen weitergegeben werden. Die Option<f64>-Rückgabe behandelt elegant den Fall „leeres Array" — du würdest in vielen Sprachen einen NaN oder eine Exception bekommen, hier hast du eine explizite None-Variante.
Die Funktions-Komposition mittel(werte)? zeigt eine schöne Anwendung des ?-Operators auf Option: wenn mittel None zurückgibt (leeres Array), propagiert die Varianz-Funktion direkt das None weiter. Keine Sonderbehandlung nötig.
Lookup-Tabelle
pub fn finde_status(
sortierte_codes: &[(u16, &str)],
code: u16,
) -> Option<&str> {
sortierte_codes
.binary_search_by_key(&code, |&(c, _)| c)
.ok()
.map(|i| sortierte_codes[i].1)
}
fn main() {
let table = [
(200, "OK"),
(404, "Not Found"),
(500, "Internal Server Error"),
];
assert_eq!(finde_status(&table, 404), Some("Not Found"));
assert_eq!(finde_status(&table, 999), None);
}Bei einer sortierten Lookup-Tabelle ist binary_search_by_key die optimale Wahl. Die Methode nimmt einen Schlüssel und eine Closure, die aus jedem Tupel den vergleichbaren Wert extrahiert. Bei einer Tabelle mit 1 000 Einträgen sind das maximal 10 Vergleiche statt 500 bei linearer Suche.
Diese Variante mit fixed-size Lookup-Array ist eine schöne Alternative zur HashMap, wenn die Tabelle statisch und klein bis mittelgroß ist. Vorteile: keine Hash-Berechnung, perfekte Cache-Lokalität, kein Allocator-Overhead. Bei sehr großen Tabellen oder dynamischen Inhalten ist HashMap dann doch wieder besser.
Network-Buffer-Verarbeitung
pub fn ist_magic_pattern(buffer: &[u8]) -> bool {
buffer.starts_with(&[0xDE, 0xAD, 0xBE, 0xEF])
}
pub fn extrahiere_payload(buffer: &[u8]) -> &[u8] {
// Erste 4 Bytes sind Magic, dann der Payload
if buffer.len() < 4 || !ist_magic_pattern(buffer) {
return &[];
}
&buffer[4..]
}
fn main() {
let pkt = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03];
assert_eq!(extrahiere_payload(&pkt), &[0x01, 0x02, 0x03]);
}Byte-Buffer-Verarbeitung ist eine der häufigsten Anwendungen von &[u8]. Die Funktion ist_magic_pattern prüft, ob ein Buffer mit einer bestimmten Byte-Sequenz beginnt — ein typisches Pattern für Protokoll-Validierung, Datei-Format-Erkennung, Frame-Detection. extrahiere_payload schneidet die ersten vier Bytes ab und gibt den Rest zurück; das ist eine reine Sub-Slice-Operation ohne Kopier-Aufwand.
&[u8] als Parameter macht die Funktionen extrem flexibel: sie funktionieren mit Vec<u8> (heap-alloziert), mit [u8; N] (stack-array), mit einem Bereich aus einem größeren Buffer (&groesserer_buf[100..200]), und sogar mit dem Ergebnis eines anderen Slice-Operations. Sie sind die universelle Form für „nimm Bytes entgegen".
Sort mit Custom-Closure
pub fn sortiere_nach_laenge(strings: &mut [String]) {
strings.sort_by_key(|s| s.len());
}
fn main() {
let mut v: Vec<String> = vec![
"ccc".into(),
"a".into(),
"bb".into(),
];
sortiere_nach_laenge(&mut v);
assert_eq!(v, vec!["a".to_string(), "bb".into(), "ccc".into()]);
}Die Sortier-Funktion nimmt einen &mut [String], was sie für In-Place-Sortierung qualifiziert. Sie modifiziert direkt den übergebenen Slice, ohne eine Kopie zu allokieren. sort_by_key ist die ergonomische Variante von sort_by: statt einer Vergleichs-Closure schreibst du eine Schlüssel-Closure, und die Sortierung nutzt automatisch die natürliche Ordnung des Schlüssel-Typs.
Wie immer bei &mut [T]-APIs gilt: die Funktion akzeptiert Vec, mutable Arrays und mutable Sub-Slices — alles, was sich zu einem mutable Slice coercen lässt. Wenn der Aufrufer einen Vec hat, kann er &mut vec direkt übergeben.
Histogramm-Generator
use std::collections::HashMap;
pub fn haeufigkeiten_str(items: &[&str]) -> HashMap<String, u32> {
let mut counts = HashMap::new();
for item in items {
*counts.entry(item.to_string()).or_insert(0) += 1;
}
counts
}
fn main() {
let v = vec!["a", "b", "a", "c", "b", "a"];
let h = haeufigkeiten_str(&v);
assert_eq!(h.get("a"), Some(&3));
assert_eq!(h.get("b"), Some(&2));
}Eine Histogramm-Funktion über einem Slice von String-Slices. Das &[&str] als Parameter sorgt dafür, dass die Funktion mit jeder Sequenz-Form aufrufbar ist — ein Vec wird per Coercion zu &[T], ein Array ebenso. Das Original-Array bleibt unverändert; das Ergebnis ist eine neue HashMap, die der Aufrufer übernimmt.
In Bibliotheks-Code würde man die Funktion generisch schreiben — sodass sie nicht nur mit &str-Elementen, sondern mit jedem hash-fähigen Typ funktioniert. Wie das geht, zeigen die Kapitel zu Generics und Traits.
Median-Berechnung
pub fn median(daten: &[i32]) -> Option<f64> {
if daten.is_empty() { return None; }
let mut sortiert = daten.to_vec();
sortiert.sort();
let mitte = sortiert.len() / 2;
if sortiert.len() % 2 == 0 {
Some((sortiert[mitte - 1] + sortiert[mitte]) as f64 / 2.0)
} else {
Some(sortiert[mitte] as f64)
}
}Die Median-Berechnung braucht einen sortierten Slice, will aber das Original nicht verändern. Die Lösung: daten.to_vec() alloziert eine eigene Kopie auf dem Heap, sortiert diese, und der Original-&[i32]-Input bleibt unangetastet. Bei großen Arrays kostet die Kopie, ist aber semantisch klar.
Eine Performance-Variante würde mit &mut [i32] arbeiten und das Original-Array sortieren — schneller, aber mit dem Trade-off, dass der Aufrufer sein Array nach dem Call sortiert hat. Die to_vec-Variante hat klarere Semantik („ich verändere dein Array nicht"), die mut-Variante ist effizienter. Die Wahl hängt vom Anwendungskontext ab.
Erste-N-Werte-Extraktion
pub fn top_n_i32(daten: &[i32], n: usize) -> Vec<i32> {
let mut v = daten.to_vec();
v.sort_by(|a, b| b.cmp(a)); // absteigend sortieren
v.into_iter().take(n).collect()
}
fn main() {
let scores = [42, 15, 87, 9, 64];
let top3 = top_n_i32(&scores, 3);
assert_eq!(top3, vec![87, 64, 42]);
}Image-Pixel-Average
pub fn durchschnitts_pixel(rgb: &[u8]) -> Option<[u8; 3]> {
if rgb.len() < 3 || rgb.len() % 3 != 0 {
return None;
}
let mut r: u64 = 0;
let mut g: u64 = 0;
let mut b: u64 = 0;
let pixel_anzahl = (rgb.len() / 3) as u64;
for chunk in rgb.chunks_exact(3) {
r += chunk[0] as u64;
g += chunk[1] as u64;
b += chunk[2] as u64;
}
Some([
(r / pixel_anzahl) as u8,
(g / pixel_anzahl) as u8,
(b / pixel_anzahl) as u8,
])
}Image-Verarbeitung ist ein typischer Anwendungsbereich für chunks_exact. RGB-Daten liegen oft als flacher Byte-Buffer vor, wobei je drei aufeinanderfolgende Bytes ein Pixel bilden. chunks_exact(3) iteriert in solchen 3-Byte-Schritten und liefert pro Schritt einen &[u8]-Slice der Länge 3 — perfekt für Pixel-weise Verarbeitung.
Der Unterschied zu chunks(3) (ohne exact) ist die Behandlung des Rests: chunks würde den letzten Teilbereich auch dann liefern, wenn er weniger als 3 Bytes hat (etwa bei einem Buffer mit 10 Bytes käme als letzter Chunk ein 1-Byte-Slice). chunks_exact ignoriert solche Rest-Bytes — was hier genau richtig ist, weil ein unvollständiges Pixel keinen Sinn macht. Die Vorab-Prüfung rgb.len() % 3 != 0 macht den Vertrag explizit.
FAQ
Soll ich &[T] oder &Vec nehmen?
Immer &[T]. Funktioniert mit Vec, Array, Sub-Slice — viel flexibler. &Vec<T> schließt Arrays und Sub-Slices aus. Clippy warnt mit clippy::ptr_arg.
Was ist der Unterschied zwischen [T] und &[T]?
[T] ist ein „unsized" Typ — der Compiler kennt seine Größe zur Compile-Zeit nicht. Du kannst keine Variable vom Typ [T] haben. Was du immer hast: eine Referenz (&[T], &mut [T]) oder einen Heap-Wrapper (Box<[T]>, Vec<T>).
iter() liefert &T, into_iter() liefert T.
Bei &[T] ist es immer iter() — du borrowst, du kannst nicht konsumieren. into_iter() würde den Slice verbrauchen, was bei einer Referenz nicht passt. Für konsumierende Iteration: ein Vec besitzen und v.into_iter() aufrufen.
Slice-Indices sind usize.
s[i] erwartet i: usize. Negative Indices gibt es nicht. Für „letztes Element" gibt es s.last() oder s[s.len() - 1].
get(i) ist die fallible Variante von [i].
s.get(i) gibt Option<&T> zurück — None bei Out-of-Bounds. Bei User-Input immer get statt direkter Index-Zugriff, um Panics zu vermeiden.
contains(&x) erwartet eine Referenz.
s.contains(&30) — das & ist nötig, weil contains einen &T-Parameter hat. Bei Strings/Slices oft Verwechslung mit dem normalen Wert.
binary_search braucht sortierten Input.
Die O(log n)-Effizienz funktioniert nur, wenn der Slice sortiert ist. Bei unsortierten Daten gibt es undefiniertes Resultat (nicht UB, aber falsche Ok/Err-Antworten). Sortier mit s.sort() (für Ord-Typen) oder s.sort_by(|a, b| ...).
Slice-Referenzen sind Copy.
Sowohl &[T] als auch &str sind 16 Bytes große, kopierbare Werte. Mehrfach in derselben Funktion zu nutzen, geht ohne Probleme — der Borrow Checker stört nicht.
Weiterführende Ressourcen
Externe Quellen
- std::primitive.slice
- std::slice — Iteratoren
- The Rust Book – Slice Type
- Clippy – ptr_arg
- Rust Reference – Slice types