Das #[derive(...)]-Attribut ist eines der mächtigsten und meistgenutzten Werkzeuge in Rust. Eine einzige Zeile vor einer Struct-Deklaration ersetzt dutzende oder hunderte Zeilen Boilerplate. Die Stdlib bringt derive-fähige Versionen der wichtigsten Standard-Traits mit: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default. Wer einen Typ definiert, beginnt fast immer mit einer Derive-Zeile. Dieser Artikel zeigt jeden dieser Traits einzeln, erklärt seine Bedeutung, welche Voraussetzungen die Felder erfüllen müssen und wann manuelle Implementierung sinnvoller ist.
Was derive macht
#[derive(Debug)] generiert eine impl Debug for Type { ... }-Implementierung automatisch zur Compile-Zeit. Vor dem derive-Makro müsstest du das von Hand schreiben — viel Boilerplate, leicht falsch.
#[derive(Debug)]
struct Person {
name: String,
alter: u32,
}
fn main() {
let p = Person { name: "Anna".into(), alter: 28 };
println!("{p:?}"); // "Person { name: \"Anna\", alter: 28 }"
}Ohne #[derive(Debug)] müsstest du implementieren:
struct Person { name: String, alter: u32 }
impl std::fmt::Debug for Person {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("Person")
.field("name", &self.name)
.field("alter", &self.alter)
.finish()
}
}Bei zwei Feldern ist der Unterschied klein — bei einem Struct mit zehn Feldern wäre die manuelle Implementation eine Strafe für Entwickler und Reviewer. derive macht das automatisch zur Compile-Zeit: das Makro generiert während des Builds genau die Boilerplate-Implementation, die du sonst von Hand schreiben müsstest.
Diese Mechanik ist nicht Magic — es ist ein Procedural Macro, das den Code zur Compile-Zeit erzeugt. Du kannst dir den generierten Code sogar mit cargo expand anschauen. Aus deiner Sicht ist es einfach kostenlos: eine Zeile statt vieler, mit identischem Laufzeit-Verhalten.
Voraussetzung: Felder müssen den Trait selbst implementieren
derive ist nicht magisch — es delegiert an die Felder. Ein Feld vom Typ T muss den gewünschten Trait selbst implementieren:
struct OhneDebug; // implementiert KEIN Debug
// #[derive(Debug)]
// struct A { feld: OhneDebug } // Compile-Fehler — Feld hat kein Debug
#[derive(Debug)]
struct B { feld: i32 } // ok — i32 hat DebugDer Compiler-Fehler ist klar:
error[E0277]: `OhneDebug` doesn't implement `Debug`
|
= help: the trait `Debug` is not implemented for `OhneDebug`
= note: required for the derived impl of `Debug` for `A`Die wichtigsten derivable Traits
Debug — Entwickler-Output
#[derive(Debug)]
struct Punkt { x: f64, y: f64 }
fn main() {
let p = Punkt { x: 1.0, y: 2.0 };
println!("{p:?}"); // "Punkt { x: 1.0, y: 2.0 }"
println!("{p:#?}"); // Pretty-Format mit Newlines
}Standard-Format mit {:?}, Pretty-Format mit {:#?}. Für Entwickler-Output, nicht End-User.
Clone und Copy
#[derive(Clone, Copy)]
struct Koord { x: i32, y: i32 }
fn main() {
let a = Koord { x: 1, y: 2 };
let b = a; // Copy — a weiterhin verwendbar
println!("{} {}", a.x, b.x);
}Clone und Copy werden in dieser Kombination häufig gemeinsam abgeleitet, weil Copy Clone voraussetzt — wer Copy will, muss auch Clone derivieren. #[derive(Clone, Copy)] ist eine eingespielte Doppelform.
Voraussetzungen: für Copy müssen alle Felder Copy sein. Sobald ein String, Vec oder anderer Heap-Typ als Feld auftaucht, ist Copy nicht möglich — der Compiler weist die Derive-Direktive ab. Clone ist toleranter: jeder Typ, der seinerseits Clone implementiert, ist als Feld erlaubt. Bei Heap-Feldern macht Clone dann eine tiefe Kopie (neuer Heap-Block, Inhalt umkopiert), während Copy nur eine flache Bit-Kopie wäre.
#[derive(Clone)]
struct Person { name: String, alter: u32 }
fn main() {
let a = Person { name: "Anna".into(), alter: 28 };
let b = a.clone(); // explizit klonen
println!("{} {}", a.name, b.name);
}PartialEq und Eq
#[derive(PartialEq, Eq)]
struct Konto { id: u64 }
fn main() {
let a = Konto { id: 42 };
let b = Konto { id: 42 };
assert_eq!(a, b); // verwendet PartialEq::eq
}Die zwei Gleichheits-Traits arbeiten zusammen. PartialEq definiert die Operatoren == und != — die abgeleitete Implementation vergleicht alle Felder feldweise und liefert true, wenn alle gleich sind. Das ist die häufigere Form und reicht für die meisten Anwendungen.
Eq ist ein Marker-Trait ohne Methoden: er sagt „diese Gleichheit ist eine echte Total-Equivalence-Relation, also reflexiv (x == x immer true), symmetrisch und transitiv". Bei f32/f64 greift das nicht, weil NaN != NaN ist — daher implementieren Floats nur PartialEq, nicht Eq. Wer einen Typ als HashMap-Key oder HashSet-Element nutzen will, braucht Eq (zusammen mit Hash), weil die Hash-Datenstrukturen sich auf die Reflexivität verlassen.
PartialOrd und Ord
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
fn main() {
let a = Version { major: 1, minor: 2, patch: 0 };
let b = Version { major: 1, minor: 3, patch: 0 };
assert!(a < b);
}PartialOrd und Ord analog zu PartialEq/Eq — der erste ist die Operator-Definition (<, <=, >, >=), der zweite ein Marker für total geordnete Typen.
Die abgeleitete Implementation vergleicht feldweise in Deklarations-Reihenfolge — also lexikographisch. Beim Version-Beispiel wird erst major verglichen; sind die gleich, geht's mit minor weiter; und nur bei Gleichheit von major und minor wird patch geprüft. Genau diese Reihenfolge passt zu Semver — wenn du eine andere Ordnung brauchst, musst du Ord manuell implementieren.
Das ist auch der Grund, warum die Reihenfolge der Felder in der Struct-Definition wichtig ist, wenn derive(Ord) benutzt wird. Wenn du erst patch und dann major deklarierst, würde die Sortierung primär nach patch laufen — was bei Versionen kein Sinn macht.
Hash
use std::collections::HashMap;
#[derive(PartialEq, Eq, Hash)]
struct UserId(u64);
fn main() {
let mut map: HashMap<UserId, String> = HashMap::new();
map.insert(UserId(42), "Anna".into());
assert_eq!(map.get(&UserId(42)), Some(&"Anna".to_string()));
}Hash plus Eq ist Pflicht für HashMap-Keys und HashSet-Elemente. Beide aus der Standard-Bibliothek.
Default
#[derive(Default, Debug)]
struct Config {
timeout_ms: u32, // Default = 0
retries: u32, // Default = 0
host: String, // Default = ""
}
fn main() {
let c = Config::default();
println!("{c:?}"); // "Config { timeout_ms: 0, retries: 0, host: \"\" }"
}Default setzt jedes Feld auf Type::default(). Funktioniert nur, wenn alle Feld-Typen Default implementieren. Für eigene Default-Werte: manuelle Impl.
Die häufigste Derive-Kombination
Für „normale" Wert-artige Typen:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
id: u64,
email: String,
}Diese fünf Traits zusammen sind das Standard-Set für Domain-Typen:
Debug— fürprintln!("{:?}", ...).Clone— explizites Duplizieren möglich.PartialEq + Eq— Vergleich mit==, HashMap-Key-fähig.Hash— HashMap/HashSet-Verwendung.
Wenn der Typ auch Copy sein soll (alle Felder Copy):
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Punkt { x: i32, y: i32 }Wenn sortierbar:
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Version { major: u32, minor: u32, patch: u32 }Wann manuelles Impl?
Trotz derive gibt es Fälle für manuelle Implementierung:
1. Custom-Display
use std::fmt;
struct Geld { eur: u32, cent: u8 }
impl fmt::Display for Geld {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{},{:02} €", self.eur, self.cent)
}
}Display hat kein derive — es muss manuell sein, weil die Stdlib keine sinnvolle Default-Formatierung wählen kann.
2. Custom-Hash für Spezial-Cases
use std::hash::{Hash, Hasher};
struct Username(String);
impl PartialEq for Username {
fn eq(&self, other: &Self) -> bool {
self.0.to_lowercase() == other.0.to_lowercase()
}
}
impl Eq for Username {}
impl Hash for Username {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_lowercase().hash(state);
}
}Wenn eq case-insensitive sein soll, muss auch hash case-insensitive sein — sonst inkonsistent. Beides manuell.
3. Felder ignorieren
derive(PartialEq) vergleicht alle Felder. Wer einzelne ignorieren will (z. B. einen Cache-Hash, der nicht zur Identität gehört), muss manuell implementieren.
Custom-Derive-Crates
Über die Stdlib-derives hinaus gibt es proc-macro-Crates mit eigenen derives:
# // Erfordert Crate: serde = { version = "1", features = ["derive"] }
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct User {
id: u64,
name: String,
}Andere häufige Custom-derives:
serde::Serialize/Deserialize— JSON, YAML, BSON, MessagePack, ...thiserror::Error— Error-Type mit Display + Error-Trait automatisch.clap::Parser/Args— CLI-Argumente parsen.derive_builder::Builder— Builder-Pattern automatisch generieren.derive_more—Add,Sub,From, ... automatisch generieren.
Diese sind keine Standard-Bibliothek, aber Quasi-Standard in modernem Rust.
Praxis: derive im echten Code
Domain-Typ mit Standard-Set
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OrderId(u64);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Order {
pub id: OrderId,
pub kunde: String,
pub total_cent: i64,
}OrderId als Newtype, Order als Field-Struct — beide mit Standard-Set für volle Verwendbarkeit.
Konfiguration mit Default
#[derive(Debug, Clone, Default)]
pub struct AppConfig {
pub host: String,
pub port: u16,
pub workers: u32,
pub tls_enabled: bool,
}
fn main() {
let cfg = AppConfig {
host: "localhost".into(),
..Default::default() // andere Felder = Default
};
println!("{cfg:?}");
}Default + Update-Syntax — sehr idiomatisch für „nur einige Felder setzen, Rest Default".
Versionierung mit Ord
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
fn main() {
let mut v = vec![
Version { major: 1, minor: 2, patch: 0 },
Version { major: 1, minor: 0, patch: 5 },
Version { major: 0, minor: 9, patch: 9 },
Version { major: 1, minor: 2, patch: 1 },
];
v.sort();
for ver in &v {
println!("{}.{}.{}", ver.major, ver.minor, ver.patch);
}
// 0.9.9, 1.0.5, 1.2.0, 1.2.1
}Lexikographische Sortierung in Deklarations-Reihenfolge — passt perfekt für Versions-Nummern.
Coord mit Copy
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Coord { pub x: f64, pub y: f64 }Klein, Copy-fähig (beide Felder Copy), keine Eq/Hash (wegen f64-NaN-Problem).
Cache-Eintrag mit Hash
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
pub user_id: u64,
pub ressource: String,
}
fn main() {
let mut cache: HashMap<CacheKey, Vec<u8>> = HashMap::new();
let key = CacheKey { user_id: 42, ressource: "profile".into() };
cache.insert(key.clone(), vec![1, 2, 3]);
assert!(cache.contains_key(&key));
}Composite-Key für HashMap — alle Felder selbst Hash + Eq, also kann der Composite-Key beide Traits derive-en.
Marker-Wert mit Default + Debug
#[derive(Debug, Default)]
pub struct Counter {
pub erfolge: u64,
pub fehler: u64,
pub gesamt: u64,
}
impl Counter {
pub fn erfolg(&mut self) {
self.erfolge += 1;
self.gesamt += 1;
}
pub fn fehler(&mut self) {
self.fehler += 1;
self.gesamt += 1;
}
}Counter::default() startet alle Zähler bei 0.
Error-Type mit Debug
#[derive(Debug)]
pub enum AppError {
NetzwerkFehler { code: u32, nachricht: String },
ParseFehler(String),
NichtGefunden,
}Enums sind ebenfalls derive-fähig. Für End-User-Output zusätzlich impl Display.
Vergleichbare Domain-IDs
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Priority(u8);
fn main() {
let mut v = vec![Priority(3), Priority(1), Priority(5), Priority(2)];
v.sort();
assert_eq!(v, vec![Priority(1), Priority(2), Priority(3), Priority(5)]);
}Newtype-Wrapper mit voller Standard-Trait-Implementierung — alle relevanten Sortier-, Hash-, Vergleichs-Operationen verfügbar.
Generischer Container — Vorschau
derive(Clone) funktioniert auch auf generischen Strukturen: pub struct Paginated<T: Clone> { items: Vec<T>, ... } lässt sich für beliebige klonbare Inhalts-Typen instanziieren. Wie <T: Clone> zu lesen ist und wie der Compiler die nötigen Bounds setzt, behandelt das Generics-Kapitel.
serde-derives für Web-API
# // Erfordert serde = { version = "1", features = ["derive"] }
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserDto {
pub id: u64,
pub username: String,
pub email: String,
}In Web-Anwendungen Standard: ein DTO-Struct mit Serialize und Deserialize für JSON-Konvertierung.
Besonderheiten
derive ist Compile-Zeit-Code-Gen.
Das #[derive(Debug)]-Attribut sagt dem Compiler, eine impl Debug-Block zu generieren. Im Maschinencode ist da kein Unterschied zu handgeschriebener Implementierung. Pure Boilerplate-Reduktion.
Felder müssen den Trait selbst implementieren.
#[derive(Hash)] struct A { f: B } funktioniert nur, wenn B: Hash. Sonst Compile-Fehler. Wer einen Trait derive-en will, muss bei allen Feldern dafür sorgen.
Standard-Set: Debug, Clone, PartialEq, Eq, Hash.
Für die meisten Domain-Typen sind diese fünf der Default. Plus Copy wenn alle Felder Copy sind, plus PartialOrd, Ord wenn sortierbar, plus Default wenn ein Zero-Konstruktor sinnvoll ist.
Copy impliziert Clone.
Du musst #[derive(Copy, Clone)] zusammen schreiben — reines Copy ohne Clone ist Compile-Fehler. Die Stdlib hat eine Trait-Hierarchie: Copy: Clone.
Eq erfordert PartialEq.
Ähnlich wie bei Copy/Clone: Eq ist Marker, PartialEq ist die eigentliche Implementierung. #[derive(PartialEq, Eq)] zusammen. Floats können nur PartialEq haben — Eq ist verboten wegen NaN.
Display ist NICHT derivable.
Anders als Debug (für Entwickler-Output) muss Display (für End-User-Output) manuell implementiert werden. Die Stdlib kann keine sinnvolle Default-Formatierung wählen — das ist eine Design-Entscheidung pro Typ.
Reihenfolge bei derive ist irrelevant.
#[derive(Clone, Debug, Hash)] und #[derive(Hash, Clone, Debug)] sind identisch. Aber idiomatisch: in der gleichen Reihenfolge wie üblich (Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default — meist alphabetisch sortiert).
Custom-derives erweitern das System.
Über proc-macros lassen sich beliebige derive-Implementierungen schreiben. serde::Serialize, thiserror::Error, clap::Parser sind klassische Beispiele — aus der externen Crate kommt der Macro, der das impl generiert.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – derive
- Rust Reference – Derive
- std::fmt::Debug
- Rust API Guidelines – Common Traits
- serde-Crate