Move ist nicht immer das, was du willst. Manchmal brauchst du zwei unabhängige Werte statt einem, der hin und herwandert. Rust bietet dafür zwei Mechanismen: den Copy-Trait als implizite Bit-Kopie bei Zuweisung (für triviale Typen wie i32) und den Clone-Trait als explizit aufgerufene Methode für tiefe Kopien (für komplexere Typen wie String oder Vec). Beide Traits sehen ähnlich aus, sind aber semantisch sehr unterschiedlich. Dieser Artikel zeigt, wann welcher Trait passt, welche Stdlib-Typen ihn implementieren, und welche Konsequenzen das Derive auf eigene Typen hat.

Copy — die implizite Bit-Kopie

Copy ist ein Marker-Trait ohne Methoden. Ein Typ, der Copy ist, wird bei Zuweisung kopiert statt gemoved:

Rust Copy-Verhalten
fn main() {
    let a: i32 = 5;
    let b = a;             // Bit-Kopie — keine Markierung von a als „leer"
    println!("{a} {b}");   // beide nutzbar: 5 5
}

Der Compiler kopiert die Bytes von a in den Stack-Slot von b. Danach gibt es zwei unabhängige Werte. Es passiert kein Drop für a separat — das wäre auch unnötig, weil i32 keinen Drop-Code hat (nichts auf dem Heap, kein File-Handle).

Welche Typen sind Copy?

Die Copy-Eigenschaft hat zwei Voraussetzungen: der Typ muss vollständig im Stack liegen (keine Heap-Allocations) und er darf keinen Drop-Impl haben (sonst würde die Bit-Kopie zu zwei Werten führen, die beide versuchen, dieselbe Ressource freizugeben). Diese zwei Bedingungen erklären die Listen unten ohne Ausnahme.

Per Default Copy:

  • Primitive: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, char.
  • Shared-Referenzen: &T für jedes T (die Referenz selbst, nicht das Ziel).
  • Funktions-Pointer: fn(...).
  • Raw-Pointer: *const T, *mut T.
  • Tupel aus Copy-Typen: (i32, f64, bool) ist Copy.
  • Arrays aus Copy-Typen: [u8; 32] ist Copy.

Niemals Copy:

  • Heap-allokierte Typen: String, Vec<T>, Box<T>, HashMap, BTreeMap. Eine Bit-Kopie würde zwei Stack-Header erzeugen, die beide auf denselben Heap-Block zeigen — das wäre ein klassisches Double-Free-Setup, das Rust kategorisch ausschließt.
  • Mutable Referenzen: &mut T. Sie haben die Aliasing-Garantie „immer nur eine zur gleichen Zeit" — eine Kopie würde diese Garantie sofort brechen.
  • Typen mit eigenem Drop-Impl. Wenn der Programmierer Drop-Logik geschrieben hat, soll sie genau einmal pro Wert laufen — bei einer Kopie wäre nicht klar, welche Instanz gemeint ist.
  • Owned Smart Pointer: Rc<T>, Arc<T>. Sie haben ein eigenes, hochoptimiertes Clone-Impl (Counter erhöhen), aber explizites .clone() ist Voraussetzung — implizite Bit-Kopie wäre fehleranfällig.

Marker-Charakter

Copy hat keine Methoden — er signalisiert dem Compiler nur „dieser Typ darf bit-kopiert werden". Die Copy-Implementierung sieht so aus:

Rust Copy-Definition
// Aus der Stdlib (vereinfacht):
pub trait Copy: Clone {}

Zwei wichtige Eigenschaften:

  • Trait ohne Methoden — pure Markierung.
  • Copy: Clone — jeder Copy-Typ ist auch automatisch Clone. Wer Copy derive-t, muss auch Clone derive-en.

Clone — die explizite Kopie

Clone hat eine Methode, die explizit aufgerufen wird:

Rust Clone-Verhalten
fn main() {
    let s1 = String::from("Hi");
    let s2 = s1.clone();        // tiefe Kopie — neuer Heap-Allokat
    println!("{s1} {s2}");      // beide unabhängig nutzbar
}

s1.clone() löst:

  • Heap-Allokation für die String-Bytes.
  • Bit-Kopie der Bytes vom alten Heap-Block in den neuen.
  • Neuer Stack-Slot s2 mit Pointer auf den neuen Block.

Resultat: zwei String-Werte, beide besitzen unabhängige Heap-Blöcke mit identischem Inhalt. Beide werden beim Scope-Ende einzeln gedroppt.

Clone-Trait-Definition

Rust Clone-Definition
// Aus der Stdlib (vereinfacht):
pub trait Clone: Sized {
    fn clone(&self) -> Self;

    // Default-Implementierung
    fn clone_from(&mut self, source: &Self) {
        *self = source.clone();
    }
}

Die zentrale Methode ist clone() -> Self. Jeder Clone-Typ kann von einer &self-Referenz aus eine neue Instanz produzieren.

clone_from ist eine Optimierung: wenn self schon Heap-Speicher hat, kann er manchmal wiederverwendet werden. Standardmäßig (Default-Impl) macht es einfach *self = source.clone(). Viele Container überschreiben das für bessere Performance.

Copy vs. Clone — die Unterschiede

AspektCopyClone
AufrufImplizit (Compiler)Explizit (.clone())
KostenBit-Kopie (immer billig)Beliebig teuer (Heap-Alloc möglich)
Wo erlaubtNur Stack-Daten ohne DropBeliebige Daten
BeziehungCopy: CloneCopy ist SubsetClone ist breiter
Methodenkeine (Marker)clone(), clone_from()
Drop-KompatibilitätTyp darf keinen Drop habenTyp darf Drop haben

Die zentrale Faustregel: Copy für triviale Werte, Clone für alles, was Heap berührt oder eigene Cleanup-Logik braucht.

derive(Copy, Clone) auf eigene Typen

Für eigene Structs und Enums kannst du die beiden Traits per Macro generieren:

Rust Eigene Copy-Typen
#[derive(Copy, Clone, Debug)]
struct Punkt {
    x: f64,
    y: f64,
}

fn main() {
    let p1 = Punkt { x: 1.0, y: 2.0 };
    let p2 = p1;                // Bit-Kopie
    println!("{p1:?} {p2:?}");  // beide nutzbar
}

Voraussetzung für derive(Copy): alle Felder müssen selbst Copy sein. Bei Punkt { x: f64, y: f64 } trifft das zu — beide Felder sind Copy-Primitive.

Sobald ein Feld nicht Copy ist:

Rust Mit String-Feld
#[derive(Clone)]    // OK
// #[derive(Copy, Clone)]   // Compile-Fehler — String ist nicht Copy
struct Person {
    name: String,
    alter: u8,
}

String ist nicht Copy, also kann Person auch nicht Copy sein. Clone alleine geht aber — und macht eine tiefe Kopie, inklusive Heap-Duplizierung des String.

Faustregeln für derive

Typ-CharakteristikEmpfehlung
Klein, alle Felder primitive#[derive(Copy, Clone)]
Mittel, mit String oder Vec#[derive(Clone)]
Hält Rohe Ressourcen (File, Socket)Kein Clone — oder bewusst manuell
Newtype über kleine Copy-Typen#[derive(Copy, Clone)]
Konfiguration mit vielen Strings#[derive(Clone)], sparsam klonen

Wann ist Copy eine schlechte Idee?

Copy ist nicht immer sinnvoll, auch wenn es technisch möglich wäre. Drei Fälle:

1. Versteckte Performance-Kosten

Rust Großer Copy-Type
#[derive(Copy, Clone)]
struct GrosserBuffer {
    daten: [u8; 4096],          // 4 KB pro Wert!
}

fn nehmen(b: GrosserBuffer) {   // Bei jedem Call: 4 KB kopiert
    println!("{}", b.daten[0]);
}

Technisch ist [u8; 4096] Copy, also kann der Struct Copy sein. Aber jeder Funktions-Call kopiert 4 KB im Stack — das ist nicht trivial. Besser: &GrosserBuffer als Parameter, oder den Struct nicht Copy machen.

2. Semantische Bedeutung von Identität

Rust Identität
// Schlechte Idee — UUID sollte nicht beliebig kopierbar sein:
// #[derive(Copy, Clone)]
// struct UserId(u64);

// Besser — Clone, aber kein Copy:
#[derive(Clone)]
struct UserId(u64);

Bei IDs, Tokens, Schlüsseln kann unbeabsichtigtes Kopieren zu logischen Bugs führen — etwa zwei Stellen, die „denselben" User haben, aber durch versehentlich kopierte IDs. Clone macht das Kopieren explizit und sichtbar.

3. Mutable Borrow notwendig

Rust Zähler
// #[derive(Copy, Clone)]
struct Zaehler { wert: u64 }

impl Zaehler {
    fn neu() -> Self { Zaehler { wert: 0 } }
    fn inc(&mut self) { self.wert += 1; }
}

Wenn Zaehler Copy wäre, würde let z2 = z1; z2.inc(); den Zähler einer Kopie inkrementieren — z1 bliebe unverändert. Bei Aggregat-Typen mit Mutation ist Copy meist unsinnig.

Custom Clone — manuelle Implementierung

derive(Clone) macht für jedes Feld einen eigenen clone-Aufruf. Manchmal will man das anders:

Rust Custom Clone
struct Cache {
    daten: Vec<u8>,
    generation: u64,
}

impl Clone for Cache {
    fn clone(&self) -> Self {
        Cache {
            daten: self.daten.clone(),
            generation: self.generation + 1,    // Klon zählt als „neue Generation"
        }
    }
}

Manuelles Clone ist selten nötig, aber praktisch bei:

  • Generation-/Version-Counter, die beim Klonen erhöht werden.
  • Klone, die zusätzlich loggen.
  • Klone, die statt Heap-Duplikat eine Reference Count erhöhen (siehe Rc/Arc).

Clone bei Rc und Arc — semantische Unterschiede

Eine Besonderheit: bei Rc<T> (Reference Counted) und Arc<T> (Atomic Reference Counted) ist .clone() billig — es erhöht nur den Reference-Counter, ohne den inneren Wert zu duplizieren.

Rust Rc-Clone
use std::rc::Rc;

fn main() {
    let original = Rc::new(String::from("Hi"));
    let klon1 = original.clone();           // Counter: 2
    let klon2 = original.clone();           // Counter: 3
    println!("Counter: {}", Rc::strong_count(&original));
    // Counter: 3
}

Alle drei Bindungen zeigen auf denselben String. Beim Drop des letzten wird der String tatsächlich freigegeben.

Hier ist clone() syntaktisch dieselbe Methode wie bei String, hat aber komplett andere Performance-Charakteristik. Wichtig zu wissen, weil .clone() auf Rc keine Tiefe-Kopie produziert. Mehr im Smart-Pointer-Kapitel.

Praxis: Copy/Clone im echten Code

Die folgenden Beispiele zeigen, wann welche Variante in echtem Code zum Einsatz kommt. Wichtig: bei jedem siehst du, wie die Wahl zwischen Copy, Clone oder „weder noch" zur API-Charakteristik passt — kleine numerische Werte werden anders behandelt als große Konfigurationen, IDs anders als Buffer.

Koordinaten-Tupel als Copy

Rust Koordinate
#[derive(Copy, Clone, Debug, PartialEq)]
struct Koord { x: f64, y: f64 }

fn distanz(a: Koord, b: Koord) -> f64 {
    let dx = a.x - b.x;
    let dy = a.y - b.y;
    (dx * dx + dy * dy).sqrt()
}

fn main() {
    let a = Koord { x: 0.0, y: 0.0 };
    let b = Koord { x: 3.0, y: 4.0 };
    println!("{}", distanz(a, b));   // 5.0
    println!("{a:?} {b:?}");          // beide noch nutzbar
}

Bei Punkt- und Vektor-Typen für Geometrie, Physik oder Grafik ist Copy praktisch immer die richtige Wahl. Die Werte sind klein (zwei f64 = 16 Bytes), enthalten keine Heap-Daten und werden in der API ständig herumgereicht. Wenn die distanz-Funktion Koord als Borrow (&Koord) statt by-value annähme, würde der Aufrufer mit *-Dereferenzen oder &-Operatoren hantieren müssen — unnötig umständlich für so kleine Werte.

Beachte auch das PartialEq-Derive: das macht den Typ vergleichbar (a == b) und ist bei Wert-Typen wie Koordinaten ein natürliches Add-on. Bei Floats ist es nur PartialEq, nicht Eq, wegen NaN — daher die Trait-Wahl.

Konfiguration mit Clone

Rust App-Config
#[derive(Clone)]
struct AppConfig {
    db_url: String,
    workers: u32,
    features: Vec<String>,
}

impl AppConfig {
    fn default_local() -> Self {
        AppConfig {
            db_url: "postgres://localhost/dev".into(),
            workers: 4,
            features: vec!["debug".into()],
        }
    }
}

fn main() {
    let cfg = AppConfig::default_local();
    // Per-Worker-Kopie der Config:
    for _ in 0..cfg.workers {
        let worker_cfg = cfg.clone();
        // ... spawn worker mit worker_cfg ...
        let _ = worker_cfg;
    }
}

Konfigurations-Structs sind ein typischer Anwendungsfall für Clone-nicht-Copy. Sie sind zu groß für Bit-Kopien (Strings, Vecs darin), aber müssen oft in mehreren Kontexten parallel existieren — z. B. eine Kopie pro Worker-Thread. Clone macht das explizit: der Code zeigt mit .clone() deutlich, dass hier eine echte Duplizierung stattfindet, mit Heap-Allokationen für alle inneren Strings und Vecs.

Bei sehr großen Configs lohnt es sich, statt clone() einen Arc<AppConfig> zwischen den Workern zu teilen — das spart die Duplizierung komplett und ist gut, wenn die Config read-only ist. Bei kleineren Configs ist clone() einfacher und passt besser zur Datenfluss-Idee „jeder Worker hat seine eigene".

UUID als Clone, nicht Copy

Rust ID-Typ
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct UserId(String);

impl UserId {
    fn neu(s: &str) -> Self { UserId(s.to_string()) }
}

fn verarbeite(id: UserId) {
    // id wird hierhin gemoved — Aufrufer muss bewusst klonen, wenn weiter benötigt
    println!("Verarbeite User: {id:?}");
}

fn main() {
    let id = UserId::neu("user-42");
    verarbeite(id.clone());      // explizit klonen
    println!("Original: {id:?}"); // ok
}

Bei ID-Typen ist es bewusst sinnvoll, sie nicht als Copy zu deklarieren, selbst wenn der innere Typ es zuließe (etwa bei UserId(u64)). Der Grund ist semantisch: eine ID ist konzeptuell ein eindeutiger Bezug, kein Wert, der frei vermehrt werden soll. Mit Clone zwingt du jeden, der eine Kopie braucht, das ausdrücklich zu schreiben — das macht den Code lesbarer und verhindert versehentliches Verbreiten.

Hier wird der innere Typ als String gewählt (für Domain-IDs wie „user-42"), was Copy ohnehin ausschließt. Bei numerischen IDs wäre der Wrapper-Newtype eine Designentscheidung — viele große Codebasen wählen bewusst Clone-only auch dort.

Numerisches Tupel-Newtype als Copy

Rust Newtype
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
struct Meter(f64);

impl Meter {
    fn plus(self, andere: Meter) -> Meter { Meter(self.0 + andere.0) }
}

fn main() {
    let strecke = Meter(100.0);
    let gesamt = strecke.plus(Meter(50.0)).plus(Meter(30.0));
    println!("{:?}", gesamt);   // Meter(180.0)
    // strecke noch nutzbar — ist Copy.
}

Das Newtype-Pattern verpackt einen Standard-Typ (f64) in einen domänenspezifischen Typ (Meter), damit Einheits-Fehler zur Compile-Zeit gefangen werden — eine Meter und eine Sekunde sollen nicht addierbar sein, obwohl beide intern f64 sind. Wenn der innere Typ Copy ist, sollte der Newtype es auch sein, damit sich die API natürlich anfühlt: strecke.plus(Meter(50.0)) darf den Original-Wert nicht enteignen.

Diese Art von Wrapper-Typen ist überall in physikalischen Berechnungen, Finanz-Code, geometrischen Bibliotheken zu finden. Mit Copy bleiben die Werte „passiv" und können beliebig herumgereicht werden, wie es bei Wertesemantik üblich ist.

Cache-Eintrag mit selektivem Clone

Rust Cache-Lookup
use std::collections::HashMap;

#[derive(Clone)]
struct CacheEintrag {
    payload: Vec<u8>,
    erstellt_ts: u64,
}

struct Cache {
    eintraege: HashMap<String, CacheEintrag>,
}

impl Cache {
    // Gibt eine geklonte Kopie — Aufrufer kann frei damit arbeiten,
    // ohne den Cache zu blockieren.
    fn holen(&self, key: &str) -> Option<CacheEintrag> {
        self.eintraege.get(key).cloned()
    }
}

Hier wird .cloned() als Adapter genutzt, um aus einem Option<&CacheEintrag> (was get zurückgibt) ein Option<CacheEintrag> zu machen. Der Aufrufer bekommt einen owned Wert, mit dem er beliebig arbeiten kann — er muss nicht auf den Cache angewiesen sein und blockiert ihn nicht durch einen langlebigen Borrow.

Diese Design-Entscheidung ist ein klassischer Trade-off: durch das Klonen wird der Cache nicht blockiert (gut für Concurrency), aber pro Lookup entsteht eine Heap-Operation (schlecht für Performance). Bei kleinen Cache-Einträgen lohnt es sich; bei sehr großen Einträgen würde man eher mit Arc<CacheEintrag> arbeiten, sodass Klone billig sind.

Builder mit Move + Clone-Möglichkeit

Rust Builder, mehrfach verwendet
#[derive(Clone)]
struct AnfrageTemplate {
    url: String,
    standard_header: Vec<(String, String)>,
}

impl AnfrageTemplate {
    fn mit_header(mut self, key: &str, wert: &str) -> Self {
        self.standard_header.push((key.into(), wert.into()));
        self
    }
}

fn main() {
    let template = AnfrageTemplate {
        url: "https://api.example.com".into(),
        standard_header: vec![("Accept".into(), "application/json".into())],
    };

    // Template mehrfach klonen, jeweils anpassen
    let a = template.clone().mit_header("X-Request-Id", "1");
    let b = template.clone().mit_header("X-Request-Id", "2");
    let _ = (a.url, b.url);
}

Bei Builder-Patterns, die ein Template mit Variationen erzeugen, ist Clone der Schlüssel. Das template enthält die Standard-Konfiguration, jeder .clone()-Aufruf produziert eine unabhängige Kopie, die dann individuell angepasst wird. Da mit_header per Move arbeitet (mut self), kann jede angepasste Version frei verwendet werden, ohne die anderen zu beeinflussen.

Dieses Pattern ist besonders verbreitet in HTTP-Client-Bibliotheken, Datenbank-Builder-APIs und Konfigurations-Templates. Wenn das Template oft kopiert wird und groß ist, kann Arc<AnfrageTemplate> mit innerer Klon-on-Write-Logik eine Performance-Optimierung sein — aber für die meisten Anwendungen ist die direkte Clone-Variante einfacher und schnell genug.

Bytes-Slice mit Clone-Vermeidung

Rust Borrow statt Clone
// Wenn die Funktion nur liest, KEIN clone nötig — &-Borrow reicht:
fn checksum(daten: &[u8]) -> u32 {
    daten.iter().map(|&b| b as u32).sum()
}

fn main() {
    let buffer = vec![1u8, 2, 3, 4, 5];
    let s1 = checksum(&buffer);    // kein clone, kein move
    let s2 = checksum(&buffer);    // funktioniert problemlos
    println!("{s1} {s2}");
}

Eine der wichtigsten Regeln in Rust-Code: wenn Borrow reicht, kein Clone. .clone() auf Heap-Typen alloziert — und in vielen Programmen ist die Allocator-Aktivität der größte Performance-Faktor. Vor jedem .clone() lohnt sich die Frage „brauche ich wirklich einen eigenen Wert, oder reicht eine Referenz?". Sehr oft reicht die Referenz.

Bei Stdlib-Methoden wie iter, as_slice, as_str siehst du dieses Prinzip bewusst umgesetzt — alle drei geben Borrows zurück, keine Klone. Das macht die Stdlib so performant: der Default ist Borrow, das Clone musst du explizit anfordern.

Snapshot-Konfiguration mit Arc

Rust Arc statt Clone
use std::sync::Arc;

#[derive(Debug)]
struct GroßeConfig {
    // ... viele große Felder ...
    grosse_string_liste: Vec<String>,
}

fn main() {
    let cfg = Arc::new(GroßeConfig {
        grosse_string_liste: vec!["x".into(); 10_000],
    });

    // Statt cfg.clone() (=20_000 Heap-Allocations):
    let cfg_kopie_1 = Arc::clone(&cfg);     // nur Counter ++
    let cfg_kopie_2 = Arc::clone(&cfg);     // nur Counter ++

    // Alle drei zeigen auf DIESELBE Config.
    let _ = (cfg, cfg_kopie_1, cfg_kopie_2);
}

Bei großen, gemeinsam genutzten Datenstrukturen ist Arc<T> ein Game-Changer. Die Arc::clone-Operation kopiert nur einen Pointer und inkrementiert einen atomaren Counter — eine Operation in wenigen CPU-Takten, völlig unabhängig von der Größe der inneren Daten. Bei einer 10 000-Eintrag-Konfiguration sparst du damit 10 000 String-Allokationen pro Clone.

Der Trade-off: alle Arc-Klone teilen sich dieselben Daten read-only. Wenn du eines der inneren Felder ändern willst, brauchst du innere Mutability (z. B. Arc<Mutex<T>>), was wieder Komplexität bringt. Für read-heavy Workloads — Konfigurationen, Lookup-Tabellen, Caches — ist Arc fast immer die richtige Wahl. Mehr Details im Smart-Pointer-Kapitel.

Besonderheiten

Copy impliziert Clone.

Jeder Copy-Typ ist auch Clone. Wer #[derive(Copy, Clone)] schreibt, hat beides — der Compiler verlangt explizit beides nebeneinander. Reines #[derive(Copy)] ohne Clone ist Compile-Fehler.

Copy ist nicht „free“ — nur „günstig“.

Eine Bit-Kopie kostet, abhängig von der Typ-Größe: 4 Bytes (i32) sind eine Register-Bewegung. 4 KB ([u8; 4096]) sind nicht trivial. Wer Copy auf große Structs setzt, verteilt versteckte Kopien überall. Faustregel: max. 16 Bytes (zwei Register) als Copy.

Clone sagt nichts über die Tiefe der Kopie.

Bei String, Vec ist .clone() eine tiefe Kopie inkl. Heap-Duplizierung. Bei Rc, Arc ist es eine flache Operation, die nur den Counter erhöht. Beide implementieren denselben Trait — die Performance ist drastisch unterschiedlich.

derive(Copy) verlangt alle Felder als Copy.

Bei verschachtelten Structs propagiert das: struct Outer { inner: Inner } kann nur Copy sein, wenn Inner Copy ist. Sobald irgendwo ein String oder Vec auftaucht, ist die Copy-Kette gebrochen.

Cell ist Copy wenn T: Copy.

Cell<i32> ist Copy, weil sein Inhalt i32 Copy ist und Cell selbst kein Drop hat. Wichtig für Interior-Mutability-Patterns auf Stack-only-Daten.

.clone() auf Option ist T.clone().

Option<T>: Clone wenn T: Clone. Some(x).clone() ergibt Some(x.clone()). None.clone() ergibt None. Sehr nützlich bei Funktions-Rückgaben mit Option-Wrapper.

String::from(s) klont, String::from(&s) auch.

Es gibt mehrere äquivalente Wege, einen &str in einen String zu konvertieren — alle allokieren auf dem Heap: s.to_string(), s.to_owned(), String::from(s), s.into(). Wahl nach Lesbarkeit, Performance identisch.

Trait-Hierarchien und Default-Derives.

Häufige Derive-Kombination für „Wert-artige" Typen: #[derive(Debug, Clone, PartialEq, Eq, Hash)]. Für triviale: zusätzlich Copy. Für sortierbar: PartialOrd, Ord. Für serde: Serialize, Deserialize. Wer alles will, bekommt schnell eine lange Derive-Liste — das ist okay.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Ownership

Zur Übersicht