„Move" ist eines der zentralen Verben in Rust — und gleichzeitig eines der missverstandensten. Ein Move ist keine teure Operation: er kopiert auf Bit-Ebene wenige Bytes (typischerweise drei Maschinenwörter für einen String oder Vec) und markiert die Original-Bindung als „nicht mehr verwendbar". Heap-Daten werden nicht angefasst, kein memcpy über tausende Bytes. Dieser Artikel zeigt im Detail, was bei let y = x; mit einem non-Copy-Typ passiert, wann Moves überall auftreten und wie partielle Moves von Struct-Feldern funktionieren.

Was ein Move wirklich tut

Bei let s2 = s1; mit s1: String passieren genau zwei Dinge:

  1. Die drei Bytes (Pointer auf Heap, Länge, Kapazität) werden vom Stack-Slot s1 zum Stack-Slot s2 kopiert.
  2. Der Compiler markiert s1 als „leer" — jede weitere Verwendung führt zu einem Compile-Fehler.

Auf dem Heap passiert nichts. Die Bytes des Strings bleiben unverändert an ihrer Adresse. Nur die Frage „wem gehören sie?" ist neu beantwortet: jetzt s2, nicht mehr s1.

Rust Was bei Move passiert
fn main() {
    let s1 = String::from("Hallo");
    // Stack: s1 = { ptr, len=5, cap=5 } -> Heap: "Hallo"

    let s2 = s1;
    // Stack: s2 = { ptr, len=5, cap=5 } -> Heap: "Hallo" (gleicher Heap-Block)
    //       s1 = (leer, nicht mehr nutzbar)

    // println!("{s1}");        // Fehler E0382
    println!("{s2}");           // ok
}

Schematisch sieht das so aus:

Rust Speicher-Diagramm
Vor dem Move:                  Nach dem Move:

Stack:                         Stack:
  s1 --+                         s1 (leer)
       |                         s2 --+
Heap:  |                              |
  +-------+                     Heap: |
  |"Hallo"| <--- ptr            +-------+
  +-------+                     |"Hallo"| <--- ptr
                                +-------+

Beim Drop am Scope-Ende wird nur einmal auf den Heap-Block freigegeben — durch s2. Hätte Rust statt Move eine Bit-Kopie inkl. Aliasing gemacht (wie in C/C++ standardmäßig), würden s1 und s2 beide auf denselben Heap-Block zeigen, und das Drop am Scope-Ende würde ihn zweimal freigeben (Double-Free). Genau das verhindert Move-Semantik.

Wann wird gemoved?

Move ist nicht auf die let-Zuweisung beschränkt — überall, wo ein Wert „seinen Platz wechselt", findet ein Move statt. Diese Verallgemeinerung verwirrt am Anfang, ist aber konsequent: aus Sicht des Compilers ist „der Wert wandert" das gleiche Konzept, egal in welcher syntaktischen Form. Die folgenden sieben Stellen sind die häufigsten, an denen du Move-Verhalten beobachten wirst.

1. Bei let-Zuweisung

Der direkteste Fall: ein neuer Name übernimmt den Wert eines bestehenden.

Rust let-Move
let a = String::from("Hi");
let b = a;
// a ist hier gemoved.

Nach der Zuweisung darf a nicht mehr gelesen werden — selbst println!("{a}") ist Compile-Fehler. Der Compiler hat in seiner internen Buchhaltung vermerkt, dass a „weg" ist, und reagiert auf jeden Lesezugriff mit E0382. Das ist ungewohnt für alle, die aus Sprachen mit GC oder impliziter Kopier-Semantik kommen — in Rust ist die alte Bindung nach dem Move tatsächlich nicht mehr verwendbar.

2. Bei Funktions-Argument

Die Übergabe eines Wertes an eine Funktion ist semantisch identisch zur Zuweisung — der Parameter wird der neue Besitzer.

Rust Funktions-Argument
fn nehmen(s: String) {
    println!("{s}");
}   // s wird hier gedroppt

fn main() {
    let text = String::from("Hi");
    nehmen(text);
    // println!("{text}");        // Fehler — gemoved in nehmen()
}

Das Beispiel zeigt einen wichtigen Punkt: Funktionsparameter haben lokale Ownership im Funktions-Body. s wird beim Eintritt zum Besitzer, und am Ende der Funktion läuft drop — der String wird freigegeben. Wer das umgehen will (etwa, weil die Funktion den Wert weiter brauchen soll), nutzt entweder Referenzen (&String / &str) oder gibt den Wert wieder zurück.

3. Bei Funktions-Rückgabe

Bei der Rückgabe eines Werts findet ein Move in die umgebende Bindung statt — der Aufrufer wird der neue Besitzer.

Rust Rückgabe
fn produzieren() -> String {
    let s = String::from("Hi");
    s                          // s wird hier zum Aufrufer gemoved
}

fn main() {
    let bekommen = produzieren();
    // bekommen ist jetzt Besitzer
}

Hier passiert technisch sogar etwas Subtileres: s lebt im Stack-Frame von produzieren, und die normale Drop-Reihenfolge würde dafür sorgen, dass s am Ende der Funktion gedroppt wird. Mit der Rückgabe wird dieser Drop übersprungen — der Wert wird stattdessen in den Stack-Frame des Aufrufers übernommen. Rust nennt diese Optimierung „RVO" (Return Value Optimization); in der Praxis bedeutet es, dass die Rückgabe von Strings und Vecs aus Funktionen keine Kopier-Operation auslöst.

4. Bei Pattern-Destrukturierung

Wenn ein Pattern Werte aus einem Container herauszieht (Tupel, Struct, Enum), werden die einzelnen Felder gemoved.

Rust Tupel-Pattern
fn main() {
    let paar = (String::from("a"), String::from("b"));
    let (x, y) = paar;          // beide Strings gemoved
    // println!("{:?}", paar);  // Fehler — gemoved.
}

Sobald alle Felder eines Tupels gemoved sind, ist das ursprüngliche Tupel nicht mehr verwendbar — auch nicht als Ganzes mit println!("{paar:?}"). Bei einem Tupel aus Copy-Typen (z. B. (i32, f64)) würde stattdessen kopiert, und beide Bindungen blieben nutzbar.

5. Bei match-Arms

Match-Arms sind eine Spezialform der Destrukturierung — wenn ein Wert gematcht wird und das Pattern Felder bindet, werden diese gemoved.

Rust Match-Move
fn main() {
    let optional: Option<String> = Some(String::from("Hi"));
    match optional {
        Some(text) => println!("{text}"),
        None => println!("nichts"),
    }
    // optional ist hier gemoved (in einen der match-Arms).
}

Beim Some(text)-Arm wird der innere String aus dem Option herausgemoved und an text gebunden. Der Trick: das original optional ist danach komplett weg, auch wenn der Code in keinem Arm landet, wo etwas mit den inneren Daten passiert. Wenn du den Wert weiter brauchst, mussst du entweder &optional matchen (Borrow) oder das innere mit clone herausholen.

6. Beim Einsortieren in Container

Container wie Vec, HashMap, VecDeque werden zu Besitzern ihrer Elemente, sobald sie eingefügt sind.

Rust Vec-Push
fn main() {
    let s = String::from("Hi");
    let mut v: Vec<String> = Vec::new();
    v.push(s);                  // s wird in den Vec gemoved
    // println!("{s}");          // Fehler.
}

Dieses Verhalten ist die Grundlage des Drop-Mechanismus für Container: wenn der Vec gedroppt wird, ruft er drop für jedes Element auf. Solange du den Container nicht zerstörst, leben deine Strings darin sicher weiter — auch wenn du im aufrufenden Code keine Bindung mehr auf sie hast.

7. Bei Closure-Capture mit move

Closures können ihre Captures auf zwei Arten halten: per Borrow (Standard) oder per Move (mit dem move-Keyword).

Rust Closure
fn main() {
    let s = String::from("Hi");
    let closure = move || println!("{s}");      // s wird in die Closure gemoved
    closure();
    // println!("{s}");                         // Fehler.
}

Das move-Keyword vor der Closure ist nicht zufällig — es macht explizit, dass die Closure ihre Captures by-value übernimmt. Ohne move würde der Compiler versuchen, mit Borrow-Capture zu arbeiten — was bei kurzlebigen Closures funktioniert, aber bei Threads scheitert, weil die Closure möglicherweise länger lebt als der ursprüngliche Wert. Bei thread::spawn und ähnlichen Konstrukten ist move daher praktisch immer notwendig.

Wann wird NICHT gemoved?

Es gibt drei Situationen, in denen die Standard-Move-Mechanik nicht greift — und es ist wichtig, sie zu kennen, weil sie das Komplement der Move-Fälle bilden. Wenn du verstehst, wann Rust nicht movet, vermeidest du die häufige Konfusion „warum kann ich den Wert hier doch weiterverwenden?".

Wenn der Typ Copy ist

Rust Copy-Typ
fn main() {
    let a: i32 = 5;
    let b = a;             // Kopie — kein Move
    println!("{a} {b}");   // 5 5 — beide nutzbar
}

Der Unterschied zwischen Move und Copy ist nicht das, was auf Maschinen-Ebene passiert — in beiden Fällen werden ein paar Bytes vom Stack-Slot der Quelle auf den der Senke kopiert. Der Unterschied ist die Frage „darf die Quelle danach noch verwendet werden?". Bei Copy ja, bei Move nein. Welche Typen sind Copy? Primitive (i32, f64, bool, char), Referenzen (&T), Tupel und Arrays aus Copy-Typen, und manuell mit #[derive(Copy)] markierte eigene Typen ohne Heap-Felder.

Bei Borrow (Referenz)

Rust Borrow
fn lesen(s: &String) {
    println!("{s}");
}

fn main() {
    let text = String::from("Hi");
    lesen(&text);              // & — kein Move, nur Borrow
    println!("{text}");        // ok, text gehört noch mir
}

Borrows sind das wichtigste Werkzeug, um Move-Semantik gezielt zu umgehen. Statt den Wert zu übergeben (Ownership-Wechsel), gibst du eine Referenz auf ihn — der Original-Besitzer bleibt unverändert. Die Funktion kann den Wert lesen (&T) oder ändern (&mut T), aber nicht behalten oder droppen. Sobald die Funktion zurückkehrt, ist die Referenz weg, und der Original-Besitzer hat ihn ungestört.

Wenn .clone() aufgerufen wird

Rust Clone
fn main() {
    let s1 = String::from("Hi");
    let s2 = s1.clone();       // Tiefe Kopie auf dem Heap
    println!("{s1} {s2}");      // Beide unabhängig nutzbar
}

.clone() ist die explizite Verdoppelungs-Methode für Non-Copy-Typen. Sie alloziert einen neuen Heap-Block, kopiert die Daten dorthin und gibt eine eigenständige Kopie zurück — beide Bindungen leben danach unabhängig voneinander. Bei String ist das ein neuer Heap-Block plus memcpy, bei Vec<u8> ebenfalls; bei verschachtelten Strukturen wie Vec<Vec<String>> rekursiv. Diese Operation ist teuer, und genau deshalb verlangt Rust ein explizites .clone() — du sollst die Kosten sehen, wenn du sie verursachst.

Partielle Moves

Aus einem Struct können einzelne Felder gemoved werden, während andere zurückbleiben — solange der Rest-Struct nicht mehr als Ganzes verwendet wird.

Rust Partielles Move
struct Person {
    vorname: String,
    nachname: String,
    alter: u32,
}

fn main() {
    let p = Person {
        vorname: String::from("Anna"),
        nachname: String::from("Müller"),
        alter: 28,
    };

    let v = p.vorname;             // v übernimmt vorname; p.vorname ist "weg"
    // println!("{}", p.vorname);   // Fehler — partiell gemoved
    println!("{}", p.nachname);    // ok — nachname noch da
    println!("{}", p.alter);        // ok — alter ist Copy
    // println!("{:?}", p);         // Fehler — p ist nicht mehr komplett
}

Der Compiler verfolgt pro Feld, ob es gemoved wurde. p.nachname und p.alter sind weiterhin verwendbar — p als Ganzes aber nicht mehr.

Partielles Move mit Pattern

Rust Destrukturierung
struct Punkt { x: f64, y: f64, label: String }

fn main() {
    let p = Punkt { x: 3.0, y: 4.0, label: String::from("A") };
    let Punkt { label, .. } = p;        // nur label moven, x und y bleiben
    println!("{label}");
    println!("{}", p.x);                 // ok — x ist Copy und nicht gemoved
    // println!("{}", p.label);          // Fehler.
}

Punkt { label, .. } destrukturiert nur das label-Feld. x und y (beide f64, also Copy) bleiben unangefastet. Wenn alle gemoveden Felder Copy wären, gäbe es nicht einmal einen partiellen Move.

Moves verhindern

Du wirst regelmäßig in die Situation kommen, dass ein Move dich blockiert: eine Funktion erwartet Ownership, aber du willst den Wert danach noch verwenden. Dafür gibt es drei klassische Lösungswege, die unterschiedliche Kosten und Auswirkungen haben — die richtige Wahl hängt davon ab, was die Funktion tatsächlich mit dem Wert macht.

1. Borrow statt Move

Rust Mit &
fn lesen(s: &String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("Hi");
    let n = lesen(&s);          // &s — kein Move
    println!("{s} hat {n} Bytes");
}

Die idiomatische Wahl, wenn die Funktion den Wert nur lesen muss. &String (oder besser &str für maximale Flexibilität) übergibt eine Referenz, ohne Ownership-Wechsel. Keine Heap-Operation, keine Allokation. Wenn die API es zulässt, sollte das immer die erste Variante sein.

2. Klonen vor Move

Rust Mit .clone()
fn verbrauchen(s: String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("Hi");
    let n = verbrauchen(s.clone());   // Klone übergeben
    println!("{s} hat {n} Bytes");
}

Wenn du die Funktion nicht anpassen kannst (z. B. weil sie aus einer fremden Library kommt) und sie wirklich Ownership erwartet, ist clone() der Ausweg. Du übergibst eine Kopie und behältst das Original. Der Preis ist die Heap-Allokation für die Kopie, plus das Kopieren der Bytes; bei großen Werten spürbar, bei kleinen vernachlässigbar. Über .clone() zu klagen ist oft vorschnell — manchmal ist es einfach die richtige Wahl.

3. Funktion gibt den Wert zurück

Rust Move-Round-Trip
fn anhaengen_und_zurueck(mut s: String, suffix: &str) -> String {
    s.push_str(suffix);
    s
}

fn main() {
    let mut s = String::from("Hi");
    s = anhaengen_und_zurueck(s, "!");
    println!("{s}");        // "Hi!"
}

Eine dritte Variante ist das „Round-Trip"-Pattern: die Funktion nimmt den Wert per Move, verändert ihn, und gibt ihn zurück. Der Aufrufer fängt den zurückgegebenen Wert in derselben Bindung wieder auf. Das funktioniert, ist aber etwas umständlich zu lesen — meistens ist ein &mut-Parameter die elegantere Lösung, weil er die zweimalige Erwähnung des Werts (rein und raus) erspart. Round-Trip lohnt sich nur, wenn die Funktion den Wert wirklich konsumieren muss (z. B. in einem Builder-Pattern) oder wenn er möglicherweise durch einen anderen Wert ersetzt wird.

Move bei Compound-Typen

Bei zusammengesetzten Typen werden alle non-Copy-Felder gleichzeitig gemoved:

Rust Compound-Move
fn main() {
    let v: Vec<String> = vec![
        String::from("a"),
        String::from("b"),
    ];
    let w = v;                  // ganzer Vec gemoved — inkl. aller Strings
    // for s in &v { ... }      // Fehler.
}

Ein Vec<String> wird als Einheit gemoved. Die einzelnen String-Werte „wandern" mit — physisch passiert aber nichts: nur die drei Vec-Header-Bytes (Pointer/Length/Capacity) werden im Stack umkopiert.

Praxis: Move im echten Code

Die folgenden Beispiele zeigen, wo Move-Semantik in realem Code ganz natürlich vorkommt — nicht als künstliche Konstruktion, sondern als die semantisch richtige Wahl für das jeweilige Problem.

Builder mit konsumierender Übergabe

Rust Builder
struct Konfig { host: String, port: u16 }

struct Server { konfig: Konfig }

impl Server {
    fn aus_konfig(k: Konfig) -> Self {
        Server { konfig: k }            // k wird in Server gemoved
    }
}

fn main() {
    let k = Konfig { host: "localhost".into(), port: 8080 };
    let s = Server::aus_konfig(k);
    // println!("{}", k.host);          // Fehler — k gemoved.
    println!("{}", s.konfig.host);
}

Hier ist Move semantisch genau richtig: der Server übernimmt die Konfig als seinen eigenen Zustand, und die separate Konfig-Variable wird logisch obsolet. Würde Rust hier kopieren statt moven, hättest du zwei lose verbundene Konfigurationen — eine in s.konfig, eine in k —, und Änderungen an einer hätten keine Wirkung auf die andere. Move stellt sicher, dass es genau einen Besitzer gibt: den Server.

Dieses Pattern findest du überall bei „Übergabe eines Aggregats in seinen Container" — eine User-Struct wird in eine UserStore-HashMap gemoved, eine Connection in einen ConnectionPool, eine Task in einen Scheduler. Die Move-Semantik unterstützt das natürliche „Container besitzt jetzt das Element".

Worker-Thread bekommt seine eigenen Daten

Rust Thread-Spawn
use std::thread;

fn main() {
    let daten = vec![1u64; 1_000_000];
    let handle = thread::spawn(move || {
        let summe: u64 = daten.iter().sum();
        println!("Summe: {summe}");
    });
    // println!("{:?}", daten.len());   // Fehler — gemoved.
    handle.join().unwrap();
}

Threads sind das paradigmatische Beispiel, in dem move zwingend ist. Der gespawnte Thread läuft potenziell länger als das umgebende Scope; wenn die Closure mit Borrow-Capture arbeiten würde, könnte der Main-Thread den Wert droppen, während der Worker noch darauf zugreifen will — der klassische Use-after-Free-Bug. Mit move wird dieser Bug zur Compile-Zeit ausgeschlossen, weil der Worker tatsächlich der Besitzer wird.

Das ist eines der schönsten Beispiele für „Memory-Safety ohne Garbage Collector": die Sprache erzwingt durch ihre Ownership-Regeln, dass Race Conditions dieses speziellen Typs unmöglich werden. Der Compiler übernimmt die Concurrency-Korrektheit-Arbeit, die in C oder C++ ein Programmierer manuell leisten müsste.

Iterator-into_iter verbrauchend

Rust into_iter
fn main() {
    let v = vec![String::from("a"), String::from("b"), String::from("c")];
    for s in v.into_iter() {        // verbrauchender Iterator
        println!("{s}");             // s ist hier owned String
    }
    // v ist hier nicht mehr da.
}

into_iter() ist eine der drei Iter-Varianten — die verbrauchende. Während iter() Referenzen liefert und der Container intakt bleibt, gibt into_iter() direkt die Werte heraus und konsumiert dabei den Container. Bei jedem Schleifendurchlauf wird ein Element aus dem Vec gemoved und an s gebunden — der Vec wird nach der Schleife leer und unbrauchbar.

Wenn du also vorhast, die Elemente weiterzuverarbeiten und brauchst sie als owned Werte, ist into_iter die effiziente Wahl. Eine Alternative wäre iter().cloned(), das den Vec intakt lässt, aber jedes Element klont — bei String-Vecs eine deutlich teurere Operation als das einfache Move.

Channel-Producer

Rust Channel-Send
use std::sync::mpsc::channel;

fn main() {
    let (tx, rx) = channel::<Vec<u8>>();

    let producer = std::thread::spawn(move || {
        for _ in 0..3 {
            let payload = vec![0u8; 1024];      // 1 KB
            tx.send(payload).unwrap();          // Move in den Channel
        }
    });

    while let Ok(daten) = rx.recv() {
        println!("Erhalten: {} Bytes", daten.len());
    }
    producer.join().unwrap();
}

Channels sind das Stdlib-Konstrukt für Producer/Consumer-Kommunikation zwischen Threads. Jeder send-Aufruf moved den Wert in den Channel — der Sender hat danach keinen Zugriff mehr. Der Receiver bekommt ihn beim recv-Aufruf zurück; auch hier ist es ein Move (nicht ein Borrow), sodass daten im Receiver-Thread vollständig sein Eigentum ist.

Genau dieses Move-Verhalten ist der Grund, warum Rust-Channels safe sind: zu jedem Zeitpunkt hat exakt einer der beiden Threads den Wert, niemals beide. Eine klassische Race Condition zwischen Sender und Receiver ist damit ausgeschlossen. Bei Channel-basierter Kommunikation kommt das Mantra „Don't communicate by sharing memory; share memory by communicating" zum Tragen.

Owner-Wechsel in einer State-Machine

Rust State-Machine
enum Verbindung {
    Inaktiv,
    Verbunden { socket: String, peer_id: u64 },
    Geschlossen { logs: Vec<String> },
}

fn schliessen(v: Verbindung) -> Verbindung {
    match v {
        Verbindung::Verbunden { socket, peer_id } => {
            let logs = vec![format!("Schloss Verbindung zu {peer_id} ({socket})")];
            Verbindung::Geschlossen { logs }
        }
        andere => andere,
    }
}

State-Machines mit Enums sind ein typischer Rust-Anwendungsfall, bei dem Move-Semantik wunderbar passt. Beim Wechsel von einem Zustand zum nächsten werden die Daten des alten Zustands destrukturiert und in den neuen Zustand verpackt — ohne Heap-Allokation, ohne Kopier-Operation, einfach durch Umverteilung der Speicher-Felder. Der socket-String und die peer_id reisen als Move-Operationen in die logs-Vec hinein (über das format!-Macro), und der Verbunden-State löst sich auf.

Diese funktionale State-Transition ist sicherer als das verbreitete „Flag-im-Struct"-Pattern aus anderen Sprachen: weil der Enum-Compiler dich zwingt, alle möglichen Zustände im match zu behandeln, kannst du keinen Übergang vergessen. Und durch die Move-Semantik kann es keinen Bug geben, in dem zwei Stellen denselben State referenzieren und sich gegenseitig invalidieren.

Funktion gibt Ownership zurück (Round-Trip-Pattern)

Rust Round-Trip
fn maybe_modify(mut s: String, condition: bool) -> String {
    if condition {
        s.push_str(" (modifiziert)");
    }
    s
}

fn main() {
    let mut s = String::from("Original");
    s = maybe_modify(s, true);
    println!("{s}");        // "Original (modifiziert)"
}

Das Round-Trip-Pattern ist eine Lösung, die funktioniert, aber selten die beste ist. Der String reist als Move in die Funktion, wird dort möglicherweise verändert, und wandert per Return zurück. Aus Aufrufer-Sicht ist das eine Re-Assignment, aus Funktions-Sicht ein normales konsumierendes Argument.

Wann lohnt sich das? Wenn die Funktion eventuell einen anderen String zurückgeben würde (etwa „nimm den Input, wenn condition true, sonst gib einen Default zurück") — dann ist Round-Trip semantisch klar. Bei reiner In-Place-Modifikation ist &mut String als Parameter die saubere Variante; sie spart das doppelte Move und macht klarer, dass derselbe String verändert wird.

Vec-Element extrahieren mit take

Rust std::mem::take
use std::mem;

struct Buffer { daten: Vec<u8> }

impl Buffer {
    fn alles_nehmen(&mut self) -> Vec<u8> {
        mem::take(&mut self.daten)      // Wert herausnehmen, durch Default ersetzen
    }
}

fn main() {
    let mut b = Buffer { daten: vec![1, 2, 3] };
    let weg = b.alles_nehmen();
    assert!(b.daten.is_empty());        // Buffer ist nicht „kaputt", nur leer
    println!("{:?}", weg);
}

std::mem::take löst ein spezifisches Problem: du hast nur eine mutable Referenz auf etwas (&mut self), aber du brauchst den owned Wert eines Felds. Direkter Move wäre Compile-Fehler, weil das Feld aus dem Receiver herauszunehmen das self-Objekt unvollständig zurücklassen würde. Stattdessen ersetzt mem::take das Feld durch den Default-Wert seines Typs (Default::default()) und gibt den ursprünglichen Wert zurück.

Bei Vec ist Vec::default() ein leerer Vec ohne Heap-Allokation — die Operation ist also extrem billig. Bei String ein leerer String. Bei eigenen Typen muss Default implementiert sein. Das Pattern ist klassisch für „leere mich aus, ich bleibe aber als Container weiter benutzbar". Sehr häufig in Stream-Verarbeitung, wo Buffer immer wieder geleert werden müssen.

Häufige Stolperfallen

Move ist günstig — denke nicht in „kopieren“.

Ein Move kopiert wenige Maschinenwörter (3 für String, Vec, Box) im Stack. Keine Heap-Operation. In Maschinencode oft 2–3 Instruktionen. Wer denkt „Move ist teuer, ich nehme lieber &", hat das mentale Modell falsch — & ist kostenfrei UND verändert Borrow-Semantik. Move ist günstig UND verändert Ownership.

{x:?} beim Print verbraucht x nicht.

println!("{x:?}", x) nutzt Debug via Referenz — x ist hinterher noch nutzbar. Print-Macros nehmen ihre Args immer per Referenz. Move passiert erst, wenn du x ohne & an eine Funktion weitergibst.

Partielle Moves verbieten Verwendung des Gesamt-Structs.

Nach let v = p.vorname; ist p als Ganzes nicht mehr nutzbar — auch nicht via println!("{p:?}"). Einzelne intakte Felder gehen aber: println!("{}", p.nachname). Wenn du Debug auf dem Struct nutzen willst, vermeide partielle Moves oder klone.

Move bei Vec[i] ist verboten.

let s = v[1]; mit v: Vec<String> ist Compile-Fehler — der Vec würde mit „Loch" zurückbleiben. Lösungen: v[1].clone(), v.swap_remove(1) (entfernt und tauscht mit letztem), mem::take(&mut v[1]) (ersetzt mit String::default()), oder v.remove(1) (kostet O(n), Elemente werden nachgerückt).

move-Closure überträgt ALLE Captures.

move || ... moved jedes eingefangene Element — auch solche, die nur gelesen werden. Wer in einer Closure mit Captures auf Mutation verzichten will, sollte erst prüfen, ob auch Borrow reicht. Bei Threads ist move aber meist nötig.

String::from(&str) alloziert; .clone() auch.

Sowohl String::from("x") als auch "x".to_string() als auch .clone() auf einem String machen Heap-Allocation. Wer das im Hot-Path vermeiden will: Slices borrowen, Cow<str> nutzen, oder einmalig Allokationen vorab machen.

let _ = x; dropt sofort.

Der Unterstrich als komplettes Pattern bindet nicht — er dropt den Wert sofort. let _ = guard.lock() gibt den Lock direkt wieder frei. Wer den Wert bis zum Scope-Ende halten will: let _guard = ... (mit Namen, Unterstrich-präfixiert für „unused").

Move kann durch derive(Copy) umgehbar gemacht werden.

Für eigene Structs aus Copy-Feldern lässt sich #[derive(Copy, Clone)] setzen — danach werden sie kopiert statt gemoved. Aber nur, wenn ALLE Felder Copy sind. String als Feld macht das unmöglich. Mehr im Copy-und-Clone-Artikel.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Ownership

Zur Übersicht