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:
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:
&Tfür jedesT(die Referenz selbst, nicht das Ziel). - Funktions-Pointer:
fn(...). - Raw-Pointer:
*const T,*mut T. - Tupel aus
Copy-Typen:(i32, f64, bool)istCopy. - Arrays aus
Copy-Typen:[u8; 32]istCopy.
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, hochoptimiertesClone-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:
// Aus der Stdlib (vereinfacht):
pub trait Copy: Clone {}Zwei wichtige Eigenschaften:
- Trait ohne Methoden — pure Markierung.
Copy: Clone— jederCopy-Typ ist auch automatischClone. WerCopyderive-t, muss auchClonederive-en.
Clone — die explizite Kopie
Clone hat eine Methode, die explizit aufgerufen wird:
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
s2mit 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
// 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
| Aspekt | Copy | Clone |
|---|---|---|
| Aufruf | Implizit (Compiler) | Explizit (.clone()) |
| Kosten | Bit-Kopie (immer billig) | Beliebig teuer (Heap-Alloc möglich) |
| Wo erlaubt | Nur Stack-Daten ohne Drop | Beliebige Daten |
| Beziehung | Copy: Clone — Copy ist Subset | Clone ist breiter |
| Methoden | keine (Marker) | clone(), clone_from() |
| Drop-Kompatibilität | Typ darf keinen Drop haben | Typ 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:
#[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:
#[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-Charakteristik | Empfehlung |
|---|---|
| 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
#[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
// 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
// #[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:
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.
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
#[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
#[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
#[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
#[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
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
#[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
// 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
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
- The Rust Book – Ways Variables and Data Interact: Clone
- std::marker::Copy
- std::clone::Clone
- Rust Reference – Copy trait
- Rust API Guidelines – Conventional Trait Implementations