Ein &mut [T] ist die mutable Variante des Element-Slice — eine Referenz auf einen Bereich, durch die du In-Place schreiben kannst. Anders als bei &[T] darf es zu jedem Zeitpunkt nur eine &mut-Referenz auf einen Wert geben (Aliasing-XOR-Mutability). Das macht mutable Slices mächtig, aber konfliktanfällig — vor allem, wenn du zwei Indices gleichzeitig modifizieren willst. Dieser Artikel zeigt die wichtigsten Methoden, das zentrale Werkzeug split_at_mut für disjunkte mutable Borrows, und durchläuft die häufigsten Borrow-Checker-Konflikte mit konkreten Lösungen.
Was &mut [T] kann
fn verdoppeln(slice: &mut [i32]) {
for x in slice.iter_mut() {
*x *= 2;
}
}
fn main() {
let mut v = vec![1, 2, 3, 4];
verdoppeln(&mut v);
assert_eq!(v, vec![2, 4, 6, 8]);
}Drei Voraussetzungen muss der Aufrufer erfüllen, damit ein &mut [T] entsteht:
Die Original-Bindung im Aufrufer muss mut sein (let mut v). Eine immutable Bindung kann keinen mutable Borrow herausgeben — das wäre ein Widerspruch zur Aussage „dieser Wert darf nicht verändert werden".
Beim Aufruf wird &mut v explizit annotiert. Anders als bei shared Borrows, wo der Compiler manchmal implizit konvertiert, musst du bei mutable Borrows immer das &mut schreiben. Das ist eine Lesbarkeitsmaßnahme: jeder, der den Code liest, sieht sofort, dass die Funktion den Wert verändern wird.
Solange das &mut [T] aktiv ist, gibt es keine andere Referenz auf dieselben Daten. Das ist die strenge Exklusivitäts-Garantie aus Aliasing-XOR-Mutability — sie macht die ganze Sicherheit erst möglich.
Die wichtigsten Methoden
iter_mut — pro Element mutieren
let mut v = vec![10, 20, 30];
for x in v.iter_mut() {
*x += 5;
}
assert_eq!(v, vec![15, 25, 35]);iter_mut() ist die idiomatische Form für „pro Element etwas verändern". Der Iterator gibt pro Schritt einen &mut T heraus — eine exklusive Referenz auf das jeweilige Element. Mit dem *x-Dereference-Operator schreibst du dann den eigentlichen Wert. Wichtig zu verstehen: zu jedem Zeitpunkt während der Schleife hält der Iterator genau eine dieser mutable Referenzen — die Aliasing-Regel wird also nie verletzt, weil die einzelnen Refs zeitlich nacheinander herausgegeben werden.
Im Vergleich zu Index-Loops wie for i in 0..v.len() { v[i] += 5; } ist iter_mut() sicherer (kein Out-of-Bounds-Risiko) und oft auch schneller (der Compiler kann besser optimieren, weil keine Bounds-Checks pro Iteration nötig sind).
swap — zwei Indices tauschen
let mut v = [1, 2, 3, 4, 5];
v.swap(0, 4);
assert_eq!(v, [5, 2, 3, 4, 1]);swap(i, j) tauscht die Elemente an den Positionen i und j direkt im Slice. Die Methode ist interessant, weil sie auf den ersten Blick mit dem Borrow Checker konfligieren müsste: zwei mutable Borrows auf v[i] und v[j] gleichzeitig. Intern nutzt swap aber mem::swap mit Raw-Pointers, die unterhalb des Borrow-Checker-Levels arbeiten — eine sichere, von der Stdlib verifizierte Abstraktion.
Bemerkenswert: der Aufruf v.swap(i, i) ist explizit erlaubt und ist ein No-Op — das Element wird mit sich selbst getauscht. Damit musst du nicht selbst auf i != j prüfen, wenn die Indices aus einer Berechnung kommen.
sort — In-Place-Sortierung
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];
v.sort();
assert_eq!(v, vec![1, 1, 2, 3, 4, 5, 6, 9]);
v.sort_by(|a, b| b.cmp(a)); // absteigend
assert_eq!(v, vec![9, 6, 5, 4, 3, 2, 1, 1]);
v.sort_by_key(|x| x.abs()); // nach Custom-Keysort ist stabil und O(n log n). Für nicht-Ord-Typen (z. B. Floats wegen NaN): sort_by mit partial_cmp oder total_cmp.
reverse — In-Place-Umkehren
let mut v = vec![1, 2, 3, 4, 5];
v.reverse();
assert_eq!(v, vec![5, 4, 3, 2, 1]);fill und fill_with — alle Werte ersetzen
let mut buffer = [0u8; 10];
buffer.fill(42);
assert_eq!(buffer, [42u8; 10]);
let mut counter = vec![0; 5];
let mut n = 0;
counter.fill_with(|| { n += 1; n });
assert_eq!(counter, vec![1, 2, 3, 4, 5]);fill und fill_with decken zwei verwandte, aber unterschiedliche Anwendungsfälle ab. fill(wert) schreibt einen Konstantwert in jedes Element — perfekt für Buffer-Reset oder Initialisierung. Bei Copy-Typen ist das eine einzige memset-ähnliche Operation; bei non-Copy-Typen wird der Wert pro Element geklont.
fill_with(closure) ist die dynamische Variante: die Closure wird pro Element aufgerufen und produziert jedes Mal einen neuen Wert. Das nutzt du, wenn der Wert sich von Element zu Element unterscheidet — etwa aufeinanderfolgende Counter, zufällige Werte aus einem RNG, oder neue Allocator-Objekte (wo fill wegen fehlendem Copy nicht funktioniert).
rotate_left / rotate_right
let mut v = [1, 2, 3, 4, 5];
v.rotate_left(2);
assert_eq!(v, [3, 4, 5, 1, 2]);
v.rotate_right(1);
assert_eq!(v, [2, 3, 4, 5, 1]);chunks_mut / windows-Äquivalente
let mut v = vec![1, 2, 3, 4, 5, 6];
for chunk in v.chunks_mut(2) {
for x in chunk {
*x *= 10;
}
}
assert_eq!(v, vec![10, 20, 30, 40, 50, 60]);chunks_mut(n) teilt den Slice in nicht-überlappende Blöcke der Größe n und gibt einen Iterator über mutable Sub-Slices zurück. Damit kannst du blockweise verarbeiten — etwa Pixel-Tripel (RGB), Sample-Frames (Audio), oder Tabelleneinträge mit fester Spaltenzahl.
Bei einer Slice-Länge, die nicht durch n teilbar ist, ist der letzte Chunk kleiner. Wenn du das nicht willst, gibt es chunks_exact_mut(n), das den Rest ignoriert. Die zwei Varianten haben subtil unterschiedliche Performance-Charakteristiken: chunks_exact_mut kann besser auto-vektorisiert werden, weil der Compiler die konstante Chunk-Größe kennt.
split_at_mut — das Schlüssel-Werkzeug
Der Borrow Checker erlaubt nur eine &mut-Referenz auf einen Wert. Was, wenn du zwei Indices gleichzeitig modifizieren willst?
fn main() {
let mut v = vec![1, 2, 3];
// let a = &mut v[0];
// let b = &mut v[2]; // Fehler — zweites &mut auf v
// *a += *b;
}Lösung: split_at_mut. Sie teilt den Slice in zwei garantiert disjunkte Bereiche.
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let (links, rechts) = v.split_at_mut(2);
// links = [1, 2], rechts = [3, 4, 5]
// beide sind &mut [i32] — disjunkt, parallel mutierbar.
links[0] = 100;
rechts[0] = 300;
assert_eq!(v, vec![100, 2, 300, 4, 5]);
}split_at_mut(n) ist Rust's Antwort auf das „ich brauche zwei mutable Borrows in dieselbe Sammlung"-Problem. Die Methode garantiert, dass die zwei zurückgegebenen Sub-Slices disjunkte Speicherbereiche abdecken — links enthält die Elemente vor Index n, rechts die ab Index n. Da sie sich nicht überlappen, kann der Compiler beruhigt zwei mutable Borrows parallel akzeptieren.
Wichtig ist, dass die Disjunktheit eine API-Garantie ist, keine vom Borrow Checker abgeleitete Erkenntnis. Die Stdlib-Implementation nutzt intern unsafe-Code mit Raw-Pointers, hat aber eine sichere API-Schale, die diese Eigenschaft beweisbar macht. Du als Aufrufer profitierst von der Sicherheit, ohne unsafe schreiben zu müssen.
Praxis-Beispiel: zwei Elemente kombinieren
fn tausche_und_addiere(v: &mut [i32], i: usize, j: usize) {
if i == j || i >= v.len() || j >= v.len() { return; }
let (lo, hi) = if i < j { (i, j) } else { (j, i) };
let (links, rechts) = v.split_at_mut(hi);
std::mem::swap(&mut links[lo], &mut rechts[0]);
}
fn main() {
let mut v = vec![10, 20, 30, 40, 50];
tausche_und_addiere(&mut v, 1, 3);
assert_eq!(v, vec![10, 40, 30, 20, 50]);
}Das idiomatische Pattern für „zwei Indices unabhängig modifizieren": Slice splitten, dann beide Hälften unabhängig bearbeiten.
split_first_mut / split_last_mut
Wenn du den ersten/letzten Wert separat brauchst:
fn main() {
let mut v = vec![1, 2, 3, 4];
if let Some((kopf, schwanz)) = v.split_first_mut() {
*kopf = 100;
for x in schwanz {
*x += 10;
}
}
assert_eq!(v, vec![100, 12, 13, 14]);
}split_first_mut() ist eine spezialisierte Variante des Splittings: sie zerlegt den Slice in das erste Element und den Rest. Das erste Element ist als &mut T (Single Element), der Rest als &mut [T] (Slice). Beide sind gleichzeitig mutable verwendbar — wieder dank der Disjunktheits-Garantie.
Analog gibt es split_last_mut(), das umgekehrt arbeitet. Diese Methoden sind besonders nützlich für Algorithmen, die einen „Anker"-Element separat vom Rest behandeln müssen — etwa Kopf-Tail-Rekursion oder Spezial-Behandlung des ersten/letzten Eintrags.
get_mut — sicherer mutable Zugriff
Wie get für lesenden Zugriff gibt es get_mut für mutable:
let mut v = vec![10, 20, 30];
if let Some(x) = v.get_mut(1) {
*x = 999;
}
assert_eq!(v[1], 999);
// Out-of-bounds — kein Panic
if let Some(x) = v.get_mut(99) {
*x = 0; // nie ausgeführt
}get_mut(i) ist die fallible Variante von slice[i] für mutable Zugriff. Sie liefert Option<&mut T>: bei gültigem Index Some(referenz), bei ungültigem None. Damit kannst du sicher mit Indices arbeiten, die aus unsicherer Quelle stammen (Nutzer-Input, Berechnungen, externe APIs).
Im Vergleich zum direkten v[i] = wert hat get_mut zwei Vorteile: erstens gibt es bei Out-of-Bounds keinen Panic, sondern eine kontrollierte None-Behandlung. Zweitens — und das ist subtiler — kannst du in einer if let Some(x) = v.get_mut(i)-Konstruktion gleich auf die Existenz prüfen und nur dann mutieren, statt zwei separate Operationen (contains_key + index) zu brauchen.
Häufige Borrow-Checker-Konflikte
Iteration während Mutation
fn main() {
let mut v = vec![1, 2, 3];
// for &x in &v { // shared borrow auf v
// if x > 1 { v.push(99); } // Fehler — &mut v + &v
// }
// Lösung: erst Indices sammeln, dann pushen
let zu_pushen: Vec<i32> = v.iter().filter(|&&x| x > 1).copied().collect();
v.extend(zu_pushen);
assert_eq!(v, vec![1, 2, 3, 2, 3]);
}Während for &x in &v läuft, ist v shared geborgt. push braucht &mut v — Konflikt. Lösung: zwei Phasen (Sammeln + Bearbeiten).
Doppel-Index in Methoden-Call
fn main() {
let mut v = vec![1, 2, 3];
// v[0] = v[2]; // Fehler? Nein — funktioniert,
// weil v[2] auf der rechten Seite zuerst gelesen wird (rvalue).
v[0] = v[2];
assert_eq!(v, vec![3, 2, 3]);
// ABER: v.push(v.len() as i32); würde NICHT gehen,
// weil v.len() braucht &v, v.push braucht &mut v.
let len = v.len() as i32;
v.push(len);
}Aufpassen: Method-Calls mit gemischten Borrows in einem Statement sind häufig Konflikt-Quellen. Lösung: Zwischenwerte vorab in lokale Bindungen extrahieren.
Mutation eines Elements während Iteration
fn main() {
let mut v = vec![1, 2, 3];
// for &x in &v {
// v[0] = x; // Fehler
// }
// Lösung — iter_mut() statt iter():
for x in v.iter_mut() {
*x *= 2;
}
assert_eq!(v, vec![2, 4, 6]);
}iter_mut() ist der idiomatische Weg, In-Place zu modifizieren. Der Borrow Checker akzeptiert das, weil iter_mut exklusive &mut T-Refs einzeln nacheinander herausgibt.
Praxis: Mutable Slices im echten Code
Bild-Bearbeitung In-Place
pub fn aufhellen(rgb: &mut [u8], prozent: u8) {
for kanal in rgb.iter_mut() {
let neu = (*kanal as u16 * (100 + prozent as u16) / 100).min(255);
*kanal = neu as u8;
}
}
fn main() {
let mut bild = vec![100u8, 50, 200, 30, 80, 150];
aufhellen(&mut bild, 20);
// Jeder Wert wurde um 20 % erhöht (mit Saturierung)
println!("{bild:?}");
}Bild-Bearbeitung In-Place ist ein typischer Anwendungsfall für &mut [u8]. Statt einen neuen Output-Buffer zu allokieren, modifizierst du den vorhandenen direkt — bei einem 10-Megapixel-Bild spart das 30 MB Allocation und das vollständige Umkopieren.
Die aufhellen-Funktion zeigt eine wichtige Praxis-Technik: die Berechnung in einen größeren Typ (u16) hochrechnen, um Overflow zu vermeiden, dann mit min(255) saturieren und zurück nach u8 konvertieren. Bei direkter u8-Arithmetik würde 100 + 200 = 44 (wrapping) — ein subtiler Bug, der bei der typ-bewussten Variante ausgeschlossen ist.
Normalisierung mit zwei Durchgängen
pub fn min_max_normalisieren(daten: &mut [f64]) {
if daten.is_empty() { return; }
let min = daten.iter().cloned().fold(f64::INFINITY, f64::min);
let max = daten.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let spanne = max - min;
if spanne == 0.0 { return; }
for x in daten.iter_mut() {
*x = (*x - min) / spanne;
}
}
fn main() {
let mut werte = vec![5.0, 10.0, 0.0, 15.0];
min_max_normalisieren(&mut werte);
assert_eq!(werte, vec![1.0/3.0, 2.0/3.0, 0.0, 1.0]);
}Min-Max-Normalisierung ist eine klassische Statistik-Operation, die in zwei Phasen läuft: erst die Statistik berechnen (Min und Max), dann mit diesen Werten den Slice transformieren. Die zwei Phasen sind durch ein paar lokale let-Bindungen getrennt, sodass die Borrows sequentiell sind und der Borrow Checker keinen Konflikt sieht.
Beachte die Defensiv-Behandlung: leeres Slice und Zero-Span führen jeweils zu einem frühen Return. Beim leeren Slice wäre die Schleife sowieso ein No-Op, aber die Min/Max-Berechnung würde mit INFINITY/NEG_INFINITY als Default arbeiten — die anschließende Division wäre durch Null und würde NaN produzieren. Das frühe Return macht die Funktion robust.
In-Place-Filter mit retain
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
v.retain(|&x| x % 2 == 0);
assert_eq!(v, vec![2, 4, 6]);
}retain ist Vec-spezifisch (nicht auf Slice direkt), aber sehr nützlich — In-Place-Filter mit O(n).
Vector-Add für zwei Slices
pub fn addiere_in_place(ziel: &mut [f64], quelle: &[f64]) {
for (z, q) in ziel.iter_mut().zip(quelle) {
*z += q;
}
}
fn main() {
let mut a = vec![1.0, 2.0, 3.0];
let b = vec![10.0, 20.0, 30.0];
addiere_in_place(&mut a, &b);
assert_eq!(a, vec![11.0, 22.0, 33.0]);
}Eine sehr typische Signatur in numerischem Code: eine Funktion, die einen Ziel-Slice mutiert (&mut [f64]) und dabei aus einer Quelle (&[f64]) liest. Die zwei Slices sind verschiedene Bindungen, die der Borrow Checker problemlos akzeptiert.
zip ist hier der Schlüssel: er erzeugt einen Iterator über Paare aus beiden Sequenzen und stoppt bei der kürzeren. Damit ist die Funktion robust auch bei unterschiedlich langen Slices — es werden nur die ersten min(ziel.len(), quelle.len()) Elemente verarbeitet. Wer das anders will, kann mit einem assert_eq!(ziel.len(), quelle.len()) zu Beginn die Länge erzwingen.
Crypt-Buffer XOR
pub fn xor_with_key(daten: &mut [u8], key: &[u8]) {
for (i, byte) in daten.iter_mut().enumerate() {
*byte ^= key[i % key.len()];
}
}
fn main() {
let mut nachricht = b"Geheimer Text".to_vec();
let key = b"key";
xor_with_key(&mut nachricht, key);
// Doppeltes XOR ergibt wieder Original
xor_with_key(&mut nachricht, key);
assert_eq!(nachricht, b"Geheimer Text");
}XOR-Verschlüsselung ist eines der einfachsten Krypto-Patterns: jedes Byte des Datenstroms wird mit einem rotierenden Schlüssel XOR-verknüpft. Die Operation ist symmetrisch — dieselbe Funktion verschlüsselt und entschlüsselt (zweimal XOR mit demselben Key ergibt das Original).
&mut [u8] macht das in-place möglich, ohne dass für die Verschlüsselung ein neuer Buffer alloziert werden muss. Bei langen Datenströmen ein wichtiger Performance-Faktor. (Anmerkung: einfaches XOR-Encryption ist kryptographisch schwach und sollte nicht für echte Sicherheit verwendet werden — hier nur als Beispiel-Pattern für In-Place-Byte-Verarbeitung.)
Sortierung mit Stat-Update
pub fn sortiere_und_messe(v: &mut [i32]) -> (i32, i32) {
v.sort();
(v[0], v[v.len() - 1]) // min, max nach Sort
}
fn main() {
let mut data = vec![3, 1, 4, 1, 5, 9];
let (min, max) = sortiere_und_messe(&mut data);
assert_eq!((min, max), (1, 9));
}Sort mutiert das Slice, danach Lese-Zugriff für Min/Max. Zwei Operationen, einfach kombiniert.
Buffer-Reverse mit Split
pub fn reverse_manuell<T>(s: &mut [T]) {
let len = s.len();
for i in 0..len / 2 {
s.swap(i, len - 1 - i);
}
}
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
reverse_manuell(&mut v);
assert_eq!(v, vec![5, 4, 3, 2, 1]);
}Eine manuelle Reverse-Implementation als Demonstration des swap-Tricks. Der Algorithmus läuft von der Mitte aus nach außen: erst Element 0 mit dem letzten getauscht, dann Element 1 mit dem vorletzten, und so weiter, bis sich beide Indices in der Mitte treffen.
Die Schlüssel-Operation ist s.swap(i, len - 1 - i). Mit zwei direkten mutable Borrows (let a = &mut s[i]; let b = &mut s[len - 1 - i];) würde der Compiler verweigern, weil er die Disjunktheit der Indices nicht prüft. swap umgeht das Problem, weil es intern mit unsafe Raw-Pointers arbeitet — die Stdlib-Implementation hat die Disjunktheit verifiziert. Für den Aufrufer ist die API safe und ergonomisch.
Audio-Sample-Normalisierung
pub fn audio_normalize(samples: &mut [f32]) {
let max_abs = samples.iter()
.map(|s| s.abs())
.fold(0.0_f32, f32::max);
if max_abs == 0.0 { return; }
let faktor = 1.0 / max_abs;
for s in samples.iter_mut() {
*s *= faktor;
}
}Audio-Normalisierung skaliert ein Sample-Array so, dass der lauteste Wert exakt 1.0 (oder -1.0) erreicht. Klassisches Read-then-Write-Pattern: erst den maximalen Absolutwert finden, dann mit dem Inversen multiplizieren.
Die Defensiv-Behandlung mit max_abs == 0.0 ist wichtig — bei einem Silence-Buffer würde sonst durch Null geteilt. Hier wird einfach zurückgekehrt, ohne den Buffer zu verändern. Das Pattern ist überall in DSP-Code zu finden, wo der Buffer durch eine Transformation läuft, die vorab eine Statistik braucht.
Index-basierte Mutation mit get_mut
pub fn setze_index<T>(slice: &mut [T], i: usize, wert: T) -> bool {
if let Some(slot) = slice.get_mut(i) {
*slot = wert;
true
} else {
false
}
}
fn main() {
let mut v = vec![1, 2, 3];
assert!(setze_index(&mut v, 1, 999));
assert!(!setze_index(&mut v, 99, 0));
assert_eq!(v, vec![1, 999, 3]);
}Ein typisches Library-Helper-Pattern: setze einen Wert an einer gegebenen Position, gib zurück, ob es geklappt hat. Statt mit slice[i] = wert (was bei Out-of-Bounds panickt) nutzt die Funktion get_mut(i) und kombiniert die Existenz-Prüfung mit der Mutation in einer Zeile.
Die bool-Rückgabe ist ein häufiges Pattern bei „falible Operations": der Aufrufer sieht direkt, ob die Operation erfolgreich war, ohne dass eine Panic oder eine Result-Erkennung im Spiel ist. Bei kritischen Operationen, wo der Fehlerfall wichtig ist, wäre Result<(), Error> mit einem aussagekräftigen Error-Typ die saubere Variante.
Häufige Stolperfallen
Nur EIN &mut [T] aktiv zur gleichen Zeit.
Aliasing-XOR-Mutability gilt auch für Slices. Wer zwei &mut-Refs auf dieselbe Sammlung braucht: split_at_mut. Disjunkte Bereiche sind erlaubt, gleiche oder überlappende nicht.
iter_mut() hält den Slice exklusiv geborgt.
Während for x in v.iter_mut() läuft, ist v als &mut geborgt. Im Loop-Body kein direkter v[...]-Zugriff möglich. Wer Index braucht: enumerate().
split_at_mut panickt bei zu großem Index.
v.split_at_mut(99) mit v.len() == 3 panickt. Für Safe-Variante: split_at_mut_checked (seit Rust 1.80) — gibt Option<(&mut [T], &mut [T])> zurück.
swap(i, j) ist Borrow-Checker-sicher.
v.swap(0, 1) funktioniert auch dann, wenn zwei &mut v[i] parallel nicht erlaubt wären. Intern nutzt es mem::swap mit Raw-Pointers — eine sichere Stdlib-Abstraktion.
sort auf Floats braucht sort_by.
f64 implementiert nicht Ord (wegen NaN). v.sort() schlägt fehl. Lösung: v.sort_by(|a, b| a.partial_cmp(b).unwrap()) oder seit Rust 1.62 v.sort_by(|a, b| a.total_cmp(b)).
chunks_mut(n) braucht n > 0.
v.chunks_mut(0) panickt. Bei dynamischem n immer vorher prüfen.
fill erfordert Clone auf T.
Bei Copy-Typen kein Problem. Bei String/Vec ohne Copy: fill_with(|| String::new()) mit einer Closure, die jedes Mal einen neuen Wert produziert.
&mut [T] ist NICHT Copy.
Anders als &[T], das Copy ist, ist &mut [T] exklusiv. Du kannst eine mutable Slice-Referenz nicht doppelt in einer Funktion verwenden, ohne Reborrowing.
Weiterführende Ressourcen
Externe Quellen
- std::primitive.slice – split_at_mut
- std::primitive.slice – iter_mut
- std::primitive.slice – sort
- std::primitive.slice – swap
- std::mem::swap