„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 passiert an mehreren Stellen — überall dort, wo ein non-Copy-Wert in eine andere Bindung „wandert":

1. Bei let-Zuweisung

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

2. Bei Funktions-Argument

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()
}

3. Bei Funktions-Rückgabe

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
}

4. Bei Pattern-Destrukturierung

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

5. Bei match-Arms

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).
}

6. Beim Einsortieren in Container

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.
}

7. Bei Closure-Capture mit move

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.

Wann wird NICHT gemoved?

Drei Fälle, in denen kein Move stattfindet:

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
}

i32 ist Copy. Statt Move passiert eine Bit-Kopie, beide Bindungen bleiben unabhängig nutzbar. Mehr im Copy-und-Clone-Artikel.

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
}

Mit & wird eine Referenz übergeben — kein Wechsel des Besitzers. Mehr im References-Kapitel.

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() erzeugt eine tiefe Kopie — der Heap-Block wird dupliziert, beide Bindungen besitzen jetzt unabhängige Werte. Teuer (Heap-Allocation), aber explizit gewünscht.

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

Drei klassische Wege, um einen Move zu umgehen, wenn du den Wert weiter brauchst:

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");
}

Beste Wahl, wenn die Funktion nur liest.

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");
}

Pragmatisch, wenn die Funktion Ownership erwartet und du den Wert weiter brauchst. Heap-Allocation.

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!"
}

Funktioniert, ist aber verbose. Meist ist &mut-Parameter besser.

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

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);
}

Konfig wird in Server gemoved — semantisch sinnvoll, weil der Server jetzt die Konfiguration besitzt.

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();
}

move || in der Closure macht klar: alle eingefangenen Variablen werden in die Closure gemoved. Damit kann der spawned Thread daten sicher besitzen — keine Race Condition möglich, weil der Main-Thread keinen Zugriff mehr hat.

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 verbraucht den Vec — jedes Element wird in s gemoved. Idiomatisch, wenn man die Elemente weiterverarbeiten und der Container danach nicht mehr gebraucht wird.

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();
}

Jeder send moved den Vec<u8> in den Channel. Im Receiver-Thread wird er aus dem Channel herausgemoved und in daten gebunden. Keine Race-Condition möglich.

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,
    }
}

Der match-Arm destrukturiert und moved socket und peer_id — sie werden in einen neuen Geschlossen-State umgepackt. Klassische funktionale State-Transition.

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)"
}

Bei diesem Pattern wird der String in die Funktion gemoved und kommt am Ende zurück. Funktioniert, aber &mut String als Parameter wäre meist eleganter.

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 ist eine Standard-Methode für „nimm den Wert heraus und ersetze durch Default". Sehr nützlich, wenn man aus einem &mut self-Receiver einen owned Wert extrahieren will, ohne den Receiver „kaputt" zu hinterlassen.

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