Vec<T> ist der wichtigste Container in Rust. Du findest ihn in praktisch jedem Programm — vom kleinen CLI-Tool bis zum verteilten System. Er ist ein heap-allozierter, wachsender Array mit O(1)-Index-Zugriff, der intern aus drei Feldern besteht: Pointer auf die Elemente, aktuelle Länge, allokierte Kapazität. Du lernst hier die vollständige API — Konstruktion, Modifikation, Iteration, Sortierung — sowie das wichtige Kapazitäts-Modell, das Performance entscheidet.

Konstruktion

Ein Vec<T> lässt sich auf mehreren Wegen erzeugen, und die Wahl ist nicht nur kosmetisch — sie hat Folgen für die Allokation. Ein leerer Vec belegt zunächst keinen Heap-Speicher: der ptr zeigt auf eine sentinelle Adresse, len und capacity sind beide 0. Erst beim ersten push (oder einer anderen wachstumsauslösenden Operation) wird ein Heap-Block angefordert. Wenn du dagegen Anfangswerte über das vec!-Makro mitgibst, erfolgt die Allokation sofort und passgenau zur Anzahl der Werte. Diese Unterscheidung wirkt klein, ist aber bei Hot-Loops oder vielen kleinen Vecs hintereinander spürbar.

Rust Vec erstellen
fn main() {
    // Leer
    let v1: Vec<i32> = Vec::new();
    let v2: Vec<i32> = vec![];

    // Mit Werten — das vec!-Makro
    let v3 = vec![1, 2, 3, 4, 5];

    // Mit Wiederholung
    let v4 = vec![0u8; 100];     // 100 Nullen

    // Mit Capacity (Performance-Hint)
    let v5: Vec<i32> = Vec::with_capacity(1000);

    // Aus Iterator
    let v6: Vec<i32> = (1..=5).collect();
}

Jede dieser Varianten erfüllt eine andere Rolle. Vec::new() und vec![] sind funktional identisch — die new-Form ist ausdrücklich, das leere Makro hingegen liest sich neben anderen vec![...]-Aufrufen konsistenter. Das vec!-Makro mit Werten ist die idiomatische Wahl für Literale; der Compiler kennt die finale Anzahl und alloziert in einem Schritt, ohne Wachstumsschritte. Die Wiederholungsform vec![0u8; 100] ist besonders nützlich für vorinitialisierte Buffer — sie nutzt intern eine schnelle Speicher-Initialisierung (bei 0u8 einen direkten memset) und ist deutlich schneller als ein Loop mit 100 push-Aufrufen.

Vec::with_capacity(n) ist der wichtigste Optimierungs-Hebel: hier wird zwar bereits Heap-Speicher für n Elemente reserviert, aber len bleibt 0 — du kannst danach n Elemente einfügen, ohne dass eine einzige Reallokation stattfindet. Der .collect()-Weg schließlich ist der Standard, sobald du Werte aus einem Iterator (z. B. einer Range, einem map, einem filter) sammelst. Intern ruft collect für Vec selbst with_capacity auf, wenn der Iterator über size_hint eine verlässliche Obergrenze meldet — Range-Iteratoren tun das, daher ist (1..=5).collect() schon optimal.

Speicher-Layout

Wer Vec wirklich verstehen will, muss sich das Speicher-Layout vor Augen führen. Ein Vec<T> ist im Speicher nicht ein einziger zusammenhängender Block, sondern eine zweistufige Struktur: ein kleiner, fester Header auf dem Stack (oder innerhalb einer Struct, in der der Vec ein Feld ist) und der eigentliche Elemente-Block auf dem Heap. Diese Trennung ist die Grundlage für drei wichtige Eigenschaften: Vecs lassen sich billig herumreichen (es werden nur 24 Bytes kopiert/gemoved), das Wachstum kann den Heap-Block neu allozieren, ohne dass sich am Header etwas ändert, und der Index-Zugriff bleibt O(1) (ptr + i * size_of::<T>()).

Rust Drei Felder
use std::mem::size_of;

fn main() {
    println!("{}", size_of::<Vec<i32>>());      // 24
    // 8 ptr + 8 len + 8 capacity
}

Die drei Felder im Header haben jeweils eine klar abgegrenzte Aufgabe. Der ptr ist ein roher Pointer auf das erste Element des Heap-Blocks — bei einem leeren Vec zeigt er auf eine spezielle Sentinel-Adresse, die nie dereferenziert wird, sodass selbst ein nie befüllter Vec keine Heap-Allokation hat. Die len ist die Anzahl der Elemente, die du tatsächlich abgelegt hast und die der Vec besitzt — auf sie beziehen sich Iteration, len(), und der Bereich, der bei einem drop aufgerufen wird. Die capacity ist die Anzahl der Slots, die im aktuellen Heap-Block Platz haben. Zwischen len und capacity liegt ungenutzter, aber bereits reservierter Speicher — Pufferraum für künftiges Wachstum.

Bei einem push, das die Kapazität überschreitet, läuft ein gut definierter Ablauf: ein neuer, größerer Heap-Block wird alloziert (typisch das Doppelte der bisherigen Kapazität), alle Elemente werden per memcpy umkopiert, der alte Block wird freigegeben, der ptr auf den neuen Block umgezogen, capacity aktualisiert. Genau dieser Umzug ist die teure Operation, die du mit with_capacity vermeiden willst.

Modifikation

Vec ist von Natur aus eine mutable Datenstruktur — die meisten interessanten Operationen brauchen eine &mut Vec<T>-Bindung. Die API lässt sich grob in drei Familien einteilen: Methoden, die Elemente hinzufügen (push, insert, extend), Methoden, die Elemente entfernen (pop, remove, swap_remove, clear, truncate, retain), und Methoden, die nur lesen oder navigieren (get, first, last, len). Die Trennung ist wichtig, weil Hinzufügen-Operationen Reallocations auslösen können und Entfernen-Operationen die Reihenfolge beeinflussen — beides hat Performance-Folgen, die du kennen solltest, bevor du eine Methode wählst.

Hinzufügen

Rust push, insert, extend
let mut v = vec![1, 2, 3];

v.push(4);                    // [1, 2, 3, 4]
v.insert(0, 0);                // [0, 1, 2, 3, 4]
v.extend([5, 6, 7]);           // [0, 1, 2, 3, 4, 5, 6, 7]
v.extend_from_slice(&[8, 9]);  // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

push(item) ist die mit Abstand häufigste Mutation: das Element wird am Ende abgelegt, len um eins erhöht. Solange len < capacity ist, kostet das nur einen Schreibvorgang — daher die Bezeichnung O(1) amortisiert. Erst wenn len == capacity erreicht ist, löst der nächste push eine Reallokation aus (Heap-Block verdoppeln, alle Elemente umkopieren). Über viele Pushes gerechnet bleibt der Schnitt bei O(1), aber einzelne Aufrufe können teuer sein.

insert(i, item) dagegen schiebt alle Elemente ab Position i um einen Slot nach hinten, bevor das neue Element abgelegt wird. Die Kosten sind O(n) — bei großen Vecs spürbar. Wenn du häufig vorne einfügst, ist Vec der falsche Container; VecDeque löst genau das mit O(1)-push_front.

extend(iter) und extend_from_slice(&[T]) sind die Bulk-Varianten. Bei extend wird der Iterator durchlaufen und jedes Element angehängt; ist der Iterator ein ExactSizeIterator, ruft Vec vorher selbst reserve mit der genauen Anzahl auf — du sparst dir das manuelle with_capacity. extend_from_slice ist die spezialisierte und schnellere Variante, wenn die Quelle bereits ein Slice ist: sie kann mit einem einzigen memcpy arbeiten, statt Element für Element.

Entfernen

Beim Entfernen gibt es eine zentrale Designentscheidung: Soll die Reihenfolge der übrigen Elemente erhalten bleiben oder nicht? Wenn ja, musst du bezahlen — alle Elemente hinter der Entfernungs-Position rücken nach. Wenn nein, gibt es einen O(1)-Trick: das letzte Element nimmt den freigewordenen Slot ein. Diese Wahl wirkt im ersten Moment ungewöhnlich, weil andere Sprachen sie kaum sichtbar machen, ist aber typisch für Rust: die teure Operation und die schnelle Alternative haben unterschiedliche Namen, damit du bewusst entscheidest.

Rust pop, remove, swap_remove
let mut v = vec![1, 2, 3, 4, 5];

let letztes = v.pop();              // Some(5), v = [1, 2, 3, 4]
let drittes = v.remove(2);          // 3, v = [1, 2, 4]
// remove ist O(n)

let mit_swap = v.swap_remove(0);    // 1, v = [4, 2]
// swap_remove ist O(1), aber Reihenfolge wird zerstört

v.clear();                          // []

pop() entfernt das letzte Element und gibt es als Option<T> zurück — None, wenn der Vec leer war. Das ist die einzige O(1)-Entfernung, die die Reihenfolge erhält, weil hinter dem letzten Element nichts liegt, das verschoben werden müsste. Pop ist daher der idiomatische Weg, einen Vec als Stack zu verwenden — kombiniert mit push hast du einen schnellen LIFO-Container.

remove(i) entfernt das Element an Position i und schiebt alle folgenden um eins nach vorne. Es erhält die Reihenfolge — was du vor allem brauchst, wenn der Vec eine sortierte Liste oder eine Sequenz mit fester Position-Bedeutung repräsentiert. Der Preis sind O(n) Kosten pro Aufruf; bei einem remove(0) auf einem 100 000-Elemente-Vec werden 99 999 Elemente kopiert.

swap_remove(i) ist die schnelle Alternative: das Element an Position i wird mit dem letzten Element des Vecs vertauscht, danach wird das letzte (also das ursprüngliche Element von Position i) abgeschnitten. Kosten: O(1). Der Haken: das ehemals letzte Element steht jetzt an Position i — die Reihenfolge ist verändert. Für ungeordnete Sammlungen (etwa eine Liste aktiver Verbindungen, die später sowieso iteriert wird) ist das ideal; für eine sortierte Bestenliste fällt es flach.

clear() setzt len auf 0, ruft drop für jedes Element auf (wichtig bei nicht-Copy-Typen wie String), behält aber die Kapazität. Der Heap-Block bleibt allokiert, du kannst danach weiter pushen, ohne neue Allokation. Das ist der Grundbaustein für das Buffer-Reuse-Pattern, das wir weiter unten sehen.

Index-Zugriff

Rust hat eine bewusste Doppelstrategie beim Lesen aus einem Vec: einen bequemen, panikenden Operator und eine explizite, sichere Methoden-Variante. Diese Trennung erlaubt knappen Code, wo der Index garantiert gültig ist, und zwingt zu expliziter Fehlerbehandlung, wo Unsicherheit besteht.

Rust Index
let v = vec![10, 20, 30];

let a = v[0];                  // 10 (panic bei Out-of-Bounds)
let b = v.get(99);             // None (sicher)
let c = v.first();              // Some(&10)
let d = v.last();               // Some(&30)
let len = v.len();              // 3
let leer = v.is_empty();       // false

v[i] ist syntaktisch der Index-Operator-Trait und ruft intern unsafe-Code, der nach einer Bounds-Prüfung das Element zurückgibt. Bei einem ungültigen Index löst er einen Panic aus — das Programm bricht den aktuellen Thread mit einer Fehlermeldung der Form index out of bounds: the len is 3 but the index is 7 ab. Das ist die richtige Wahl, wenn du logisch sicherstellen kannst, dass der Index gültig ist (etwa weil du gerade v.len() geprüft hast).

v.get(i) ist die fehlertolerante Form: sie gibt Option<&T> zurück und ist die einzige korrekte Wahl, wenn der Index aus unsicherer Quelle stammt (Nutzer-Input, externe API, parsed Daten). Du musst danach explizit ein if let Some(x) = v.get(i) oder match schreiben — das ist mehr Code, aber kein Panic in Sicht.

first() und last() sind beide ebenfalls Option<&T> und damit auch auf leeren Vecs sicher — sie sind die idiomatischen Helfer für die zwei häufigsten Zugriffe. len() und is_empty() schließlich sind reine Header-Reads und sehr billig — keine Heap-Operation, nur ein Vergleich des len-Feldes.

Iteration

Iteration über einen Vec ist eine der häufigsten Operationen überhaupt, und Rust unterscheidet hier sehr genau, was mit den Elementen passiert, während du sie durchläufst. Es gibt drei Iter-Modi, und welcher der richtige ist, hängt davon ab, ob du den Vec danach noch brauchst und ob du die Elemente verändern willst. Diese Unterscheidung ist eine direkte Folge des Ownership-Modells: jeder Modus respektiert eine andere Borrow-Variante.

Rust Drei Iter-Modi
let v = vec![10, 20, 30];

// Über Referenzen
for x in v.iter() {
    println!("{x}");           // x: &i32
}

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

// Mutable Iteration
let mut m = vec![1, 2, 3];
for x in m.iter_mut() {
    *x *= 10;
}

// Verbrauchende Iteration
for x in v.into_iter() {
    println!("{x}");           // x: i32, v wird konsumiert
}

iter() gibt einen Iterator über geliehene Referenzen &T zurück. Der Vec bleibt unangetastet, du kannst ihn vor und nach dem Loop weiter verwenden. Das ist der Default — wenn du nur lesen willst, ist iter() (oder die Kurzform &v im for) immer die richtige Wahl.

iter_mut() gibt &mut T zurück und erlaubt dir, jedes Element in-place zu verändern. Du brauchst dafür eine mutable Bindung des Vecs (let mut m). Die Schreibweise *x *= 10 zeigt eine typische Stolperfalle: der Iterator-Wert ist &mut i32, nicht i32 — du musst mit dem *-Operator dereferenzieren, bevor du den eigentlichen Wert anfasst.

into_iter() konsumiert den Vec und gibt die Elemente direkt (nicht als Referenz) heraus. Nach dem Loop ist der Vec weg — v darf nicht mehr verwendet werden. Das ist die richtige Wahl, wenn du die Elemente weiterverarbeiten und der Vec danach nicht mehr gebraucht wird; du sparst dir damit das .clone() jedes Elements. Seit Edition 2021 ist for x in v die Kurzform für for x in v.into_iter() — vorher war es eine subtile Bug-Quelle, weil for x in &v und for x in v zwar unterschiedlich, aber visuell ähnlich aussehen.

Die enumerate()-Erweiterung ist orthogonal: sie nimmt einen beliebigen Iterator und liefert (Index, Element)-Tupel — der eigentliche Iter-Modus bleibt unverändert (iter().enumerate() liefert (usize, &T), iter_mut().enumerate() liefert (usize, &mut T)).

Iterator-Adapter

Über die drei Iter-Modi hinaus stellt der Iterator-Trait eine ganze Familie von Adapter-Methoden bereit, mit denen du Transformationen, Filter, Aggregationen und Sucher zu Pipelines verketten kannst. Diese Adapter sind das Herzstück der idiomatischen Rust-Verarbeitung und ersetzen viele klassische Loops fast vollständig. Sie sind lazyfilter oder map allein machen nichts, sie bauen nur einen Iterator. Erst eine Terminal-Operation (collect, sum, for, count, ...) lässt die Pipeline tatsächlich laufen und konsumiert sie.

Rust Funktional
let v = vec![1, 2, 3, 4, 5];

let summe: i32 = v.iter().sum();
let max = v.iter().max();
let gefiltert: Vec<i32> = v.iter().filter(|&&x| x > 2).copied().collect();
let quadriert: Vec<i32> = v.iter().map(|x| x * x).collect();
let position = v.iter().position(|&x| x == 3);    // Some(2)
let alle = v.iter().all(|&x| x > 0);              // true
let irgendeines = v.iter().any(|&x| x > 4);       // true

Auffällig sind die kleinen syntaktischen Eigenheiten. Bei filter(|&&x| x > 2) siehst du zwei & in der Closure — die äußere Referenz stammt von iter() (&T), die innere ist das Pattern, das die Referenz „aufdröselt" und x als nackten Wert i32 bindet. Ohne dieses Pattern müsstest du im Body *x > 2 schreiben. Bei position und find ist es derselbe Effekt — du arbeitest mit Werten statt Referenzen, was bei Copy-Typen die Lesbarkeit erhöht. Bei größeren, nicht-Copy-Typen würde man stattdessen |x| x.feld > 2 schreiben und die Referenz beibehalten.

copied() ist eine kleine, oft übersehene Methode: sie wandelt einen Iterator über &T in einen Iterator über T um, sofern T: Copy. Ohne copied() würde collect::<Vec<i32>>() aus einem Iterator<Item = &i32> nicht funktionieren, weil der Ziel-Vec i32 enthält, nicht &i32. Für nicht-Copy-Typen heißt der entsprechende Adapter cloned() — er klont jedes Element, ist also teurer.

Bei den Terminal-Operationen gibt es eine wichtige Familienteilung: sum, count, min, max aggregieren zu einem Wert, collect sammelt in einen Container, und all/any/position/find sind Short-Circuit-Operationen — sie laufen die Pipeline nur so weit ab, bis das Ergebnis feststeht. Bei v.iter().any(|&x| x > 4) wird abgebrochen, sobald das erste passende Element gefunden ist, nicht der gesamte Vec gescannt.

Sortierung

Sortierung ist eine In-Place-Operation auf einem &mut Vec<T> — der Vec wird verändert, kein neuer Vec entsteht. Das Verfahren ist intern Timsort (eine optimierte Mischung aus Merge-Sort und Insertion-Sort), die in der Praxis sehr schnell ist und bei vorsortierten oder fast-sortierten Daten besonders glänzt. Die Sortierung ist stabil — gleiche Elemente behalten ihre ursprüngliche Reihenfolge, was bei Sortierung nach einem Schlüssel wichtig ist, wenn andere Felder als sekundäre Ordnungs-Information gelten sollen.

Rust sort
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];
v.sort();                                   // [1, 1, 2, 3, 4, 5, 6, 9]

v.sort_by(|a, b| b.cmp(a));                 // absteigend
v.sort_by_key(|x| x.abs());                  // nach Abs-Wert
v.sort_by(|a, b| a.partial_cmp(b).unwrap()); // für f64 (kein Ord)

sort() ohne Argumente nutzt die natürliche Ordnung der Elemente und braucht daher T: Ord. Für alle Integer-Typen, String, &str und Tupel/Arrays aus Ord-Typen geht das direkt. Bei Floats (f32, f64) gibt es das berüchtigte NaN-Problem: NaN ist mit nichts vergleichbar, daher implementieren Floats nur PartialOrd, nicht Ord. Drei Wege aus der Falle: sort_by(|a, b| a.partial_cmp(b).unwrap()) ist die schnelle Lösung, panickt aber bei NaN; sort_by(|a, b| a.total_cmp(b)) (ab Rust 1.62) ordnet NaN konsequent ein und ist die idiomatische Wahl seitdem; alternativ kann man NaN vorab herausfiltern.

sort_by(closure) erlaubt eine eigene Vergleichs-Funktion — du gibst einen Ordering-Wert zurück (Less, Equal, Greater). Praktisch für absteigende Sortierung (b.cmp(a)) oder zusammengesetzte Schlüssel.

sort_by_key(closure) ist die ergonomische Alternative, wenn du nach einem abgeleiteten Schlüssel sortieren willst (Länge, Absolutwert, ein Struct-Feld). Achtung beim Performance-Profil: sort_by_key ruft die Schlüssel-Funktion mehrfach pro Element auf — wenn die Berechnung teuer ist, ist sort_by_cached_key die bessere Wahl, weil sie die Schlüssel einmal vorberechnet und cached.

Dedupe nach Sort

dedup() ist die klassische Begleit-Operation zu sort(). Sie geht den Vec einmal durch und wirft aufeinanderfolgende Duplikate weg — Element für Element vergleicht sie mit dem Vorgänger und behält nur den ersten Treffer. Das ist O(n) und in-place, also sehr schnell. Allerdings funktioniert es nur dann zuverlässig „alle Duplikate weg", wenn der Vec vorher sortiert ist; in einem unsortierten Vec mit Inhalt [1, 2, 1, 2] würde dedup nichts ändern, weil keine zwei gleichen Werte direkt nebeneinander stehen.

Rust dedup
let mut v = vec![1, 2, 2, 3, 3, 3, 4];
v.dedup();                  // [1, 2, 3, 4]
// dedup entfernt nur AUFEINANDERFOLGENDE Duplikate.
// Davor sortieren, wenn alle Duplikate weg sollen.

Slicing

Eine der wichtigsten Eigenschaften von Vec ist, dass er sich transparent wie ein Slice verhält. Über den Deref<Target = [T]>-Trait kennt der Compiler die Beziehung: überall, wo eine Funktion &[T] erwartet, kannst du eine &Vec<T> übergeben — Rust führt die Konvertierung automatisch durch (sogenannte Deref-Coercion). Das hat eine konkrete Konsequenz für API-Design: Funktions-Parameter solltest du immer als &[T] deklarieren, niemals als &Vec<T>, weil &[T] flexibler ist (akzeptiert auch Arrays, statische Slices, sub-slices).

Rust Slice von Vec
let v = vec![1, 2, 3, 4, 5];
let s: &[i32] = &v;                  // gesamter Vec
let t: &[i32] = &v[1..4];             // Sub-Slice

// Idiomatische Funktion mit Slice-Parameter:
fn summe(s: &[i32]) -> i32 { s.iter().sum() }
let n = summe(&v);

Im Detail: &v erzeugt zunächst eine &Vec<i32>. Wenn diese an einen Parameter vom Typ &[i32] übergeben wird, ruft der Compiler implizit <Vec as Deref>::deref(&v) auf, was eine &[i32] produziert, die auf denselben Heap-Speicher zeigt. Kein Kopieren, keine Allokation — nur ein Header-Wechsel von „24 Bytes (ptr, len, cap)" zu „16 Bytes (ptr, len)". Die &v[1..4]-Syntax erzeugt einen Sub-Slice, der einen Teilbereich beschreibt; intern ist das wieder nur ein Pointer und eine Länge, kein Kopieren der Daten.

Die Faustregel daraus: &[T] als Funktions-Parameter, Vec<T> nur als Rückgabewert oder wenn du Ownership übernehmen musst. Diese Trennung macht deine Funktionen testbarer (du kannst sie mit Array-Literalen aufrufen) und vermeidet unnötig restriktive Signaturen.

Wachstum und Kapazität

Wachstum ist der teuerste Aspekt von Vec, und gleichzeitig der, den viele Anfänger ignorieren. Solange len < capacity, kostet ein push nur einen Schreibvorgang. Wenn len == capacity erreicht ist, muss Vec einen neuen Heap-Block anfordern, alle bisherigen Elemente kopieren und den alten Block freigeben — eine Operation in O(n). Damit das nicht bei jedem push passiert, verdoppelt Vec die Kapazität bei jedem Wachstumsschritt; so kommen über n Pushes nur O(log n) Reallocations zusammen, und die Gesamtkosten bleiben amortisiert linear.

Rust Kapazität
let mut v: Vec<i32> = Vec::new();
println!("{} / {}", v.len(), v.capacity());      // 0 / 0

v.push(1);
println!("{} / {}", v.len(), v.capacity());      // 1 / 4

for i in 0..1000 {
    v.push(i);
}
println!("{} / {}", v.len(), v.capacity());
// typisch 1001 / 1024 — Vec verdoppelt Kapazität bei Bedarf

Auffällig im Beispiel ist der Sprung von 0 auf 4 beim ersten Push: Vec startet nicht mit Kapazität 1, sondern wählt einen kleinen Default (typisch 4 Elemente bei T mit size_of::<T>() ≤ 1024), weil eine isolierte Allokation für ein einzelnes Element fast genauso teuer ist wie für vier. Danach folgt die saubere Verdopplung — 4 → 8 → 16 → 32 → 64 → 128 → 256 → 512 → 1024.

Mit Vec::with_capacity(n) kannst du diesen ganzen Wachstumsprozess überspringen: der Vec startet direkt mit Kapazität n, und solange du nicht mehr als n Elemente einfügst, findet keine einzige Reallokation statt. Das ist die wichtigste Performance-Tuning-Maßnahme für Vec überhaupt. Wenn du die finale Größe gar nicht weißt, kannst du immer noch eine sinnvolle Schätzung angeben — auch eine grobe Untergrenze hilft schon, weil sie die ersten paar Wachstumsschritte einspart.

Wer mit dem Thema tiefer einsteigen will (inklusive reserve, reserve_exact, shrink_to_fit), findet die Details im eigenen Artikel zu Kapazität und Reallocation.

Praxis: Vec im echten Code

Die folgenden Beispiele sind keine konstruierten Lehrbuch-Aufgaben, sondern Muster, die du in produktivem Rust-Code immer wieder findest. Bei jedem geht es weniger um die API selbst — die wurde oben gezeigt — als um die richtige Kombination der Methoden und die Entscheidung, warum Vec hier der passende Container ist.

Buffer für Network-Read

Rust Network-Buffer
use std::io::Read;

pub fn lese_alle<R: Read>(reader: &mut R) -> std::io::Result<Vec<u8>> {
    let mut buffer = Vec::with_capacity(8192);
    reader.read_to_end(&mut buffer)?;
    Ok(buffer)
}

Beim Lesen aus Netzwerk-Sockets, Pipes oder Streams weißt du selten die exakte Datenmenge vorab — die Vec<u8>-Wahl ist hier ideal, weil sie dynamisch wachsen kann. Trotzdem ist eine Anfangskapazität sinnvoll: 8 KB ist eine typische Page-Size und passt zu den meisten Netzwerk-Reads. read_to_end liest in einer Schleife in den Vec und vergrößert ihn bei Bedarf — wenn du keine Kapazität vorgibst, durchläuft er die ersten Wachstumsschritte (4, 8, 16, 32, ..., 8192) für nichts. Bei Streams im Megabyte-Bereich macht das einen sichtbaren Unterschied.

Worker-Queue

Rust Job-Queue
pub struct WorkerQueue<T> {
    jobs: Vec<T>,
}

impl<T> WorkerQueue<T> {
    pub fn neu() -> Self { WorkerQueue { jobs: Vec::new() } }
    pub fn enqueue(&mut self, job: T) { self.jobs.push(job); }
    pub fn dequeue(&mut self) -> Option<T> {
        if self.jobs.is_empty() { None }
        else { Some(self.jobs.remove(0)) }
    }
}

Dieses Beispiel zeigt eine Queue, die funktional korrekt, aber suboptimal ist. push ans Ende ist O(1) amortisiert, aber remove(0) an den Anfang muss bei jedem dequeue alle übrigen Elemente um einen Slot nach vorne kopieren — bei einer Queue mit 1000 Jobs sind das 999 Kopier-Operationen pro dequeue. Solange die Queue klein bleibt (unter ein paar Dutzend Elementen), ist das tolerierbar; für ernsthafte Job-Queues ist VecDeque die richtige Wahl, weil sie push_back und pop_front beide in O(1) hat. Das Beispiel ist hier bewusst gewählt, um den Unterschied sichtbar zu machen — du würdest es so nur dann schreiben, wenn die Queue garantiert klein bleibt.

Filter-Pipeline

Rust filter + collect
pub fn aktive_user_ids(users: &[(u64, bool)]) -> Vec<u64> {
    users.iter()
        .filter(|(_, aktiv)| *aktiv)
        .map(|(id, _)| *id)
        .collect()
}

fn main() {
    let u = [(1, true), (2, false), (3, true)];
    assert_eq!(aktive_user_ids(&u), vec![1, 3]);
}

Filter-Pipelines sind eine der häufigsten Anwendungsformen von Iterator-Adaptern. Wichtig hier ist die Reihenfolge: erst filter (wirft den Großteil der Elemente weg), dann map (transformiert nur die übrigen). Würdest du erst alle Elemente mappen und dann filtern, wäre das genauso korrekt, aber teurer. collect() am Ende braucht eine Ziel-Typ-Information — die kommt hier aus der Rückgabe-Signatur -> Vec<u64>, sodass der Compiler die Spezialisierung kennt. Ohne Rückgabe-Annotation müsstest du collect::<Vec<u64>>() schreiben.

Histogram-Sammlung

Rust Frequency
pub fn top_chars(text: &str, top_n: usize) -> Vec<(char, u32)> {
    let mut counts: Vec<(char, u32)> = Vec::new();
    for c in text.chars() {
        if let Some(entry) = counts.iter_mut().find(|(ch, _)| *ch == c) {
            entry.1 += 1;
        } else {
            counts.push((c, 1));
        }
    }
    counts.sort_by(|a, b| b.1.cmp(&a.1));
    counts.truncate(top_n);
    counts
}

Das Histogramm-Pattern zeigt einen interessanten Trade-off. Statt einer HashMap<char, u32> wird hier ein Vec<(char, u32)> mit linearer Suche genutzt — iter_mut().find(...) durchläuft im Worst Case alle bisherigen Einträge pro Zeichen. Bei kurzen Texten mit wenigen distinkten Zeichen (etwa ASCII-only) ist das tatsächlich schneller als eine HashMap, weil HashMap-Lookups konstante Kosten für das Hashen und Cache-Misses haben, die bei wenigen Buckets nicht amortisiert werden. Bei Texten mit Hunderten distinkter Zeichen kippt das Verhältnis, und HashMap wird klar überlegen. Die anschließende sort_by und truncate(top_n)-Kombination ist das idiomatische „Top-N"-Muster — sortieren, dann den Schwanz abschneiden.

Paginierung

Rust Pagination
pub fn paginieren<T: Clone>(daten: &[T], seite: usize, pro_seite: usize) -> Vec<T> {
    daten.iter()
        .skip(seite * pro_seite)
        .take(pro_seite)
        .cloned()
        .collect()
}

fn main() {
    let alle: Vec<i32> = (1..=100).collect();
    let seite_2 = paginieren(&alle, 1, 10);
    assert_eq!(seite_2, (11..=20).collect::<Vec<_>>());
}

Pagination ist ein Standard-Anwendungsfall, der die Lazy-Natur der Iteratoren gut ausnutzt. skip(n) durchläuft die ersten n Elemente, ohne sie zu materialisieren, take(m) begrenzt auf höchstens m weitere — beides ist O(1) bei Vec-Iteratoren mit nativer nth-Implementierung. .cloned() kopiert die übrigen Elemente, was notwendig ist, weil die Rückgabe einen eigenen Vec mit Ownership zurückgibt, der vom Slice unabhängig leben muss. Beachte: für Copy-Typen wie i32 wäre .copied() etwas billiger, weil es keinen Clone-Trait-Aufruf braucht; für Clone-Typen wie String ist .cloned() der einzige Weg.

CSV-Parsing

Rust CSV-Reader
pub fn parse_zeilen(text: &str) -> Vec<Vec<String>> {
    text.lines()
        .map(|zeile| zeile.split(',').map(str::trim).map(String::from).collect())
        .collect()
}

fn main() {
    let csv = "a, b, c\n1, 2, 3";
    let parsed = parse_zeilen(csv);
    assert_eq!(parsed, vec![
        vec!["a".to_string(), "b".into(), "c".into()],
        vec!["1".into(), "2".into(), "3".into()],
    ]);
}

Verschachtelte collect-Aufrufe sind eines der ergonomisch stärksten Pattern in Rust. Der äußere collect baut den Vec<Vec<String>>, der innere (implizit über die Closure) den Vec<String> pro Zeile. Bemerkenswert ist, dass der Compiler den Ziel-Typ der inneren Sammlung selbständig aus der Rückgabe-Signatur ableiten kann — du musst nirgendwo Typ-Annotationen verteilen. Die drei verketteten map-Aufrufe (splittrimString::from) sind ein typisches String-Verarbeitungs-Muster: Bytes/Sub-Slices in besitzte Strings überführen. Beachte, dass das jede Zelle neu alloziert — bei sehr großen CSV-Dateien wäre eine Variante mit &str und Lifetime-Annotationen (oder die csv-Crate) speichersparender.

Akkumulation

Rust Running Sum
pub fn kumulativ(werte: &[f64]) -> Vec<f64> {
    let mut summe = 0.0;
    werte.iter()
        .map(|&v| { summe += v; summe })
        .collect()
}

fn main() {
    assert_eq!(kumulativ(&[1.0, 2.0, 3.0]), vec![1.0, 3.0, 6.0]);
}

Kumulative Summen oder Running-Aggregates lassen sich elegant ausdrücken, aber dieses Beispiel zeigt eine kleine Stolperfalle. Die Closure |&v| { summe += v; summe } mutiert eine Variable außerhalb ihrer selbst (summe), was bedeutet, dass die Closure den FnMut-Trait implementiert und Rust intern eine mutable Referenz auf summe festhält. Solange die Pipeline sequenziell läuft (was Iteratoren in Rust per Definition tun), ist das absolut sicher. Trotzdem ist die idiomatische Variante für diesen Anwendungsfall der scan-Adapter, der genau für state-tragende Akkumulation gedacht ist — er gibt sowohl den State als auch die Möglichkeit zum frühen Abbruch über Option. Die Map-Variante ist hier nur deshalb gewählt, weil sie kürzer zu lesen ist.

Bulk-Insert mit Reservierung

Rust Reserve + Bulk
pub fn fortsetzen<T: Clone>(target: &mut Vec<T>, source: &[T]) {
    target.reserve(source.len());
    target.extend_from_slice(source);
}

Dieses Pattern kommt zum Einsatz, wenn du erst zur Laufzeit weißt, wie viele Elemente noch dazukommen werden. reserve(n) ist die Lauf-Zeit-Variante von with_capacity — es stellt sicher, dass nach dem Aufruf mindestens n zusätzliche Elemente ohne Reallokation Platz haben. extend_from_slice ist dann die optimale Folgemethode, weil sie weiß, dass die Quelle ein Slice mit bekannter Länge ist, und intern einen einzigen memcpy ausführt statt Element für Element zu pushen. Bei T: Copy ist der Unterschied zu extend(source.iter()) nicht groß; bei aufwendigeren Clone-Typen kann er erheblich sein.

Buffer-Reuse

Rust Reuse-Pattern
pub fn process_in_loop<F: FnMut(&[u8])>(daten: &[&[u8]], mut callback: F) {
    let mut buffer = Vec::with_capacity(4096);
    for chunk in daten {
        buffer.clear();
        buffer.extend_from_slice(chunk);
        buffer.push(b'\n');
        callback(&buffer);
    }
}

Das Buffer-Reuse-Pattern ist eines der wichtigsten Performance-Tricks bei Vec überhaupt. In der Schleife wird der gleiche buffer immer wieder befüllt, ausgewertet und durch clear() zurückgesetzt — clear macht len = 0, behält aber den Heap-Block. Im zweiten Durchgang ist die Kapazität schon groß genug, das extend_from_slice und push passen ohne neue Allokation. Über tausende Durchläufe gerechnet vermeidet das tausende Allokationen und Freigaben, was sowohl die Laufzeit als auch die Speicher-Fragmentierung spürbar verbessert. Du siehst dasselbe Pattern überall, wo per-Item-Buffer in einem Loop verarbeitet werden — Parser, Renderer, Protokoll-Encoder.

Interessantes

vec![...]-Makro ist der Standard für Literale.

vec![1, 2, 3] ist die kompakteste Form. vec![0; n] für n Nullen — initialisiert mit Wiederholung. Beide werden zur Compile-Zeit zu effizientem Code.

with_capacity vermeidet Reallocations.

Wenn die finale Größe bekannt ist (oder grob schätzbar), Vec::with_capacity(n) nutzen. Spart bei großen Vecs erhebliche Performance — keine Reallocations + Kopier-Operationen während des Wachstums.

iter / iter_mut / into_iter.

Drei Iterations-Modi: Borrow / mutable Borrow / verbrauchend. for x in &v und for x in v.iter() sind identisch. for x in v ist into_iter() (Edition 2021+).

swap_remove ist O(1), remove ist O(n).

swap_remove(i) tauscht das Element an Position i mit dem letzten und entfernt das letzte. Reihenfolge wird zerstört, aber 1000× schneller bei großen Vecs als remove(i), das alle Elemente nachrückt.

Vec coerciert zu &[T].

Funktions-Parameter sollten &[T] sein, nicht &Vec<T>. Flexibler — akzeptiert auch Arrays und Sub-Slices. Clippy warnt mit clippy::ptr_arg.

v.sort() braucht T: Ord.

Floats sind nicht Ord (NaN-Problem). Bei Vec<f64> nutzt du sort_by(|a, b| a.partial_cmp(b).unwrap()) oder seit Rust 1.62 sort_by(|a, b| a.total_cmp(b)).

clear() behält die Kapazität.

Nach v.clear() ist v.len() == 0, aber v.capacity() bleibt unverändert. Sehr nützlich für Buffer-Reuse in Loops — keine neue Allocation für die nächste Befüllung.

collect auf Iterator ergibt Vec.

Der häufigste Container-Termin. iter.collect::<Vec<_>>() oder mit Type-Annotation let v: Vec<i32> = iter.collect(). Funktioniert auch für andere Container — HashMap, HashSet, BTreeMap, ...

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Collections

Zur Übersicht