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 vielen Feldern oder vielen Typen wird das schnell unübersichtlich. derive macht das automatisch.
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);
}Copy setzt voraus, dass alle Felder Copy sind. Sobald ein String oder Vec als Feld auftaucht, ist Copy nicht möglich — Clone aber schon.
#[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
}PartialEq— Vergleich mit==und!=. Auf Felder reflektiert.Eq— Marker-Trait: „dies ist eine Total-Equivalence-Relation". Voraussetzung: keinf32/f64-Feld (wegen NaN). Pflicht, wenn der Typ als HashMap-Key dienen soll.
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);
}derive(PartialOrd, Ord) vergleicht feldweise in Deklarations-Reihenfolge (lexikographisch). Erst major, bei Gleichheit minor, dann patch. Sehr nützlich bei sortierbaren Domänen-Typen.
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
#[derive(Debug, Clone)]
pub struct Paginated<T: Clone> {
pub items: Vec<T>,
pub gesamt: u64,
}derive(Clone) funktioniert generisch — verlangt aber T: Clone als Bound (vom Compiler automatisch hinzugefügt).
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