Eine Default-Methode ist eine Trait-Methode mit vorgegebenem Body. Implementierende Typen müssen sie nicht selbst liefern — der Default-Body wird verwendet, sofern der Typ ihn nicht explizit überschreibt. Damit kannst du Traits bauen, die eine kleine Pflicht-Schnittstelle haben (die jeder Typ implementieren muss) und viele Convenience-Methoden dazu (die aus den Pflicht-Methoden abgeleitet sind). Das Iterator-Trait der Stdlib ist das Paradebeispiel: nur next() ist Pflicht, ~70 weitere Methoden (map, filter, collect, fold, ...) sind Default-Implementierungen darauf.
Was Default-Methoden sind
Bei einer normalen Trait-Methode steht nur die Signatur, der Body kommt vom Implementierer. Bei einer Default-Methode steht Signatur und Body im Trait — der Implementierer kann den Default übernehmen oder eigene Logik liefern.
trait Tier {
// Pflicht-Methode — Implementierer MUSS sie liefern
fn name(&self) -> &str;
// Default-Methode — Implementierer KANN sie überschreiben
fn beschreibung(&self) -> String {
format!("Ein Tier namens {}", self.name())
}
}
struct Hund;
impl Tier for Hund {
fn name(&self) -> &str { "Rex" }
// beschreibung NICHT überschrieben — nutzt Default
}
fn main() {
let h = Hund;
println!("{}", h.beschreibung()); // "Ein Tier namens Rex"
}fn beschreibung(&self) -> String { ... } hat einen Body. Der Hund implementiert nur die Pflicht-Methode name; die Default-Methode wird automatisch übernommen.
Das Ergebnis: weniger Boilerplate für implementierende Typen. Das Trait gibt eine reiche Schnittstelle, der Typ muss nur die Kern-Methoden liefern, der Rest kommt gratis.
Default überschreiben
Der Implementierer kann jede Default-Methode überschreiben, indem er sie einfach in seinen impl-Block schreibt.
# trait Tier {
# fn name(&self) -> &str;
# fn beschreibung(&self) -> String {
# format!("Ein Tier namens {}", self.name())
# }
# }
struct Katze;
impl Tier for Katze {
fn name(&self) -> &str { "Mimi" }
// Default-Methode mit eigener Implementation überschrieben
fn beschreibung(&self) -> String {
format!("Eine elegante Katze namens {}", self.name())
}
}
fn main() {
let k = Katze;
println!("{}", k.beschreibung()); // "Eine elegante Katze namens Mimi"
}Katze::beschreibung überschreibt den Default. Wenn jemand katze.beschreibung() aufruft, wird die spezialisierte Version aufgerufen, nicht der Trait-Default. Für die Pflicht-Methode name gibt es keinen Default, also musst du sie sowieso liefern.
Dieses Pattern erlaubt graduelle Spezialisierung: einfache Typen nehmen die Defaults, komplexere überschreiben sie. Der Trait-Designer gibt sinnvolle Standards vor; Implementierer optimieren bei Bedarf.
Defaults nutzen andere Trait-Methoden
Default-Methoden können andere Trait-Methoden aufrufen — sowohl Pflicht- als auch andere Default-Methoden.
trait Counter {
fn current(&self) -> u32; // Pflicht
fn step(&mut self); // Pflicht
// Default nutzt Pflicht-Methoden
fn advance(&mut self, n: u32) {
for _ in 0..n {
self.step();
}
}
// Default nutzt andere Default
fn advance_and_get(&mut self, n: u32) -> u32 {
self.advance(n);
self.current()
}
}
struct Tick { wert: u32 }
impl Counter for Tick {
fn current(&self) -> u32 { self.wert }
fn step(&mut self) { self.wert += 1; }
}
fn main() {
let mut t = Tick { wert: 0 };
let final_wert = t.advance_and_get(5);
assert_eq!(final_wert, 5);
}advance ruft step() auf — eine andere Methode des gleichen Traits. advance_and_get ruft advance und current auf. Diese Verkettung ist mächtig: aus wenigen Pflicht-Methoden entsteht ein reiches API von Default-Methoden, die alle auf den Kern-Methoden aufbauen.
Wichtig: wenn Tick jetzt advance überschreiben würde, würde die überschriebene Version sowohl bei direkten Aufrufen als auch beim indirekten Aufruf aus advance_and_get verwendet. Die Dispatching-Mechanik nutzt immer die konkrete Implementation des Typs.
Das Iterator-Pattern
Das wichtigste Stdlib-Beispiel für Default-Methoden ist der Iterator-Trait.
// Vereinfachte Darstellung (echter Iterator-Trait ist komplexer)
trait MeinIterator {
type Item;
// Pflicht-Methode — die einzige
fn next(&mut self) -> Option<Self::Item>;
// Default-Methoden — aufbauend auf next
fn count(mut self) -> usize where Self: Sized {
let mut n = 0;
while self.next().is_some() { n += 1; }
n
}
fn collect_to_vec(mut self) -> Vec<Self::Item> where Self: Sized {
let mut v = Vec::new();
while let Some(item) = self.next() { v.push(item); }
v
}
fn first(mut self) -> Option<Self::Item> where Self: Sized {
self.next()
}
}next() ist die einzige Pflicht-Methode. Alle anderen Methoden — count, collect, first, map, filter, fold, sum, product — sind Default-Implementierungen darauf. Im echten Iterator-Trait der Stdlib sind das ~70 Default-Methoden.
Das ist das Template-Method-Pattern: ein Algorithmus wird im Trait formuliert, die konkreten Schritte werden vom Implementierer geliefert. Wer Iterator implementiert, schreibt nur next() — bekommt aber das komplette Iterator-API gratis dazu.
Der where Self: Sized-Constraint ist wichtig: konsumierende Methoden (mit self ohne &) funktionieren nur bei Sized-Typen, nicht bei dyn Iterator. Mit diesem Constraint bleibt der Trait trotzdem object-safe (siehe dyn-Artikel).
Default-Implementations als Optimierung
Manchmal ist die Default-Implementation funktional korrekt, aber suboptimal. Implementierende Typen können sie mit spezialisierten, schnelleren Varianten überschreiben.
// Imagine: ein Container-Trait
trait Container {
type Item;
fn iter(&self) -> Box<dyn Iterator<Item = &Self::Item> + '_>;
// Default: zählt durch Iteration
fn len(&self) -> usize {
self.iter().count()
}
// Default: linear scan
fn contains(&self, item: &Self::Item) -> bool
where Self::Item: PartialEq
{
self.iter().any(|x| x == item)
}
}
// Implementierender Typ kann length effizient überschreiben:
struct MyVec<T> { data: Vec<T> }
impl<T> Container for MyVec<T> {
type Item = T;
fn iter(&self) -> Box<dyn Iterator<Item = &T> + '_> {
Box::new(self.data.iter())
}
// Effiziente Override
fn len(&self) -> usize {
self.data.len() // O(1) statt O(n) durch Iteration
}
}Der Default für len() zählt durch Iteration — O(n). MyVec überschreibt mit der direkten Vec-Länge — O(1). Konsumenten profitieren von der spezialisierten Variante, ohne dass sie davon wissen müssen.
Dieses Pattern ist sehr typisch: das Trait gibt funktional korrekte aber langsame Defaults, Implementierer können performant spezialisieren. Bei Iterator::size_hint() ist das genau die Mechanik — der Default sagt „weiß ich nicht", spezialisierende Typen geben genaue Größen.
Defaults mit where-Bounds
Default-Methoden können eigene where-Bounds haben — Constraints, die nur für diese spezifische Methode gelten.
trait Container {
type Item;
fn iter(&self) -> Box<dyn Iterator<Item = Self::Item> + '_>;
// Diese Default funktioniert nur, wenn Item Display ist
fn join(&self, separator: &str) -> String
where Self::Item: std::fmt::Display
{
let items: Vec<String> = self.iter()
.map(|x| format!("{x}"))
.collect();
items.join(separator)
}
}join hat einen eigenen Bound Self::Item: Display. Bei Typen, deren Items nicht Display sind, ist die Methode einfach nicht verfügbar — der Compiler lehnt den Aufruf ab. Bei Typen mit Display-Items steht sie zur Verfügung.
Diese Form ist in der Stdlib häufig. Iterator::sum() etwa hat where Self::Item: Sum<...> — funktioniert nur für summierbare Item-Typen. Bei anderen ist die Methode nicht aufrufbar.
Designprinzipien für Defaults
Minimale Pflicht-API
// Schlecht: viele Pflicht-Methoden
trait BadTrait {
fn a(&self);
fn b(&self);
fn c(&self);
fn d(&self);
fn e(&self);
// Implementierer muss alle 5 schreiben
}
// Gut: eine Pflicht-Methode, der Rest als Default
trait GoodTrait {
fn core_op(&self) -> i32; // Pflicht
fn doubled(&self) -> i32 { self.core_op() * 2 }
fn tripled(&self) -> i32 { self.core_op() * 3 }
fn quad(&self) -> i32 { self.core_op() * 4 }
fn is_positive(&self) -> bool { self.core_op() > 0 }
}Gute Trait-Designs minimieren die Pflicht-API und maximieren die Default-API. Implementierer haben weniger Arbeit, das Trait selbst ist reicher. Bei der Pflicht-API sollte gelten: jede Pflicht-Methode ist wirklich essentiell und kann nicht aus anderen abgeleitet werden.
Defaults sollten Standard-Verhalten geben
trait Greeting {
fn name(&self) -> &str;
// Default: generische, neutrale Grußformel
fn greet(&self) -> String {
format!("Hallo, {}!", self.name())
}
}Default-Implementations sollten sinnvolle, generische Standards sein — der „good enough"-Fall für die meisten Typen. Spezielle Typen überschreiben dann. Wenn der Default nur eine panic! oder eine ungültige Operation ist, ist die Methode eigentlich keine Default, sondern eine schlecht designte Pflicht-Methode.
Defaults dokumentieren
Bei Library-Traits sollten Defaults dokumentiert sein: welcher Aufwand (O-Notation), welche Voraussetzungen, ob ein Override sinnvoll ist.
trait Container {
type Item;
fn iter(&self) -> Box<dyn Iterator<Item = Self::Item> + '_>;
/// Anzahl der Items im Container.
///
/// Die Default-Implementation iteriert und zählt — **O(n)**.
/// Implementierungen sollten dies überschreiben, wenn die
/// Länge in O(1) verfügbar ist (z.B. Vec, Slice).
fn len(&self) -> usize {
self.iter().count()
}
}Solche Doc-Kommentare helfen Implementierern zu entscheiden, ob ein Override nötig ist. Klassische Stdlib-Dokumentation macht das durchgehend.
Praxis: Default-Methoden im echten Code
Logger mit Default-Level-Hierarchie
pub trait Logger {
// Pflicht: die Kern-Log-Methode
fn log(&self, level: &str, msg: &str);
// Defaults: Convenience-Methoden über log
fn debug(&self, msg: &str) { self.log("DEBUG", msg); }
fn info(&self, msg: &str) { self.log("INFO", msg); }
fn warn(&self, msg: &str) { self.log("WARN", msg); }
fn error(&self, msg: &str) { self.log("ERROR", msg); }
}
pub struct StdLogger;
impl Logger for StdLogger {
fn log(&self, level: &str, msg: &str) {
println!("[{level}] {msg}");
}
}
fn main() {
let l = StdLogger;
l.info("App gestartet");
l.warn("Hohe Auslastung");
l.error("Fehler aufgetreten");
}Ein Pflicht-Methode (log), vier Default-Methoden (debug, info, warn, error) als Convenience. Wer Logger implementiert, schreibt eine Methode und bekommt das komplette Level-API gratis.
Validator mit Aggregation
pub trait Validator {
type Input;
// Pflicht: liefert eine Liste von Fehler-Strings
fn check(&self, input: &Self::Input) -> Vec<String>;
// Default: ist gültig wenn keine Fehler
fn is_valid(&self, input: &Self::Input) -> bool {
self.check(input).is_empty()
}
// Default: erster Fehler als Option
fn first_error(&self, input: &Self::Input) -> Option<String> {
self.check(input).into_iter().next()
}
}Ein Pflicht-Methode (check), zwei Convenience-Defaults darauf. Konsumenten können je nach Bedarf das volle Fehler-Array, einen einzelnen Bool oder den ersten Fehler abfragen.
Cache-Trait mit Hit-Statistiken
pub trait Cache<K, V> {
// Pflicht-Methoden
fn get(&self, key: &K) -> Option<V>;
fn put(&mut self, key: K, value: V);
// Default: get-or-compute Pattern
fn get_or_compute<F: FnOnce() -> V>(&mut self, key: K, compute: F) -> V
where K: Clone, V: Clone
{
if let Some(v) = self.get(&key) {
v
} else {
let v = compute();
self.put(key, v.clone());
v
}
}
}Zwei Pflicht-Methoden, eine Default für das beliebte „Cache-or-Compute"-Pattern. Das Trait kombiniert grundlegende Operationen mit höheren Abstraktionen.
Konvertierungs-Trait
pub trait Convertable {
// Pflicht: zu i64 konvertieren
fn to_i64(&self) -> i64;
// Defaults darauf basierend
fn to_i32(&self) -> i32 { self.to_i64() as i32 }
fn to_u32(&self) -> u32 { self.to_i64() as u32 }
fn to_f64(&self) -> f64 { self.to_i64() as f64 }
fn is_zero(&self) -> bool { self.to_i64() == 0 }
fn is_positive(&self) -> bool { self.to_i64() > 0 }
}
impl Convertable for &str {
fn to_i64(&self) -> i64 {
self.parse().unwrap_or(0)
}
}
fn main() {
assert_eq!("42".to_i32(), 42);
assert_eq!("42".to_f64(), 42.0);
assert!("0".is_zero());
assert!("100".is_positive());
}Eine Pflicht-Methode (to_i64), fünf Defaults darauf. Implementierende Typen brauchen nur die Kern-Konvertierung; alle abgeleiteten Operationen sind gratis.
Mit Generic-Konfiguration
pub trait Formatter {
fn format(&self, value: i32) -> String;
}
pub trait Reporter {
// Pflicht: liefert die Daten
fn get_data(&self) -> Vec<i32>;
// Default: nutzt einen Formatter (Generic-Parameter)
fn report<F: Formatter>(&self, fmt: &F) -> Vec<String> {
self.get_data()
.iter()
.map(|v| fmt.format(*v))
.collect()
}
}Default-Methode mit eigenem Generic-Parameter — die Methode ist generisch über den Formatter-Typ. So bleibt das Trait flexibel: verschiedene Konsumenten können verschiedene Formatter nutzen.
Optionaler Hook
pub trait Worker {
// Pflicht: Hauptarbeit
fn execute(&mut self);
// Optionaler Hook — Default tut nichts
fn on_start(&mut self) {}
fn on_finish(&mut self) {}
// Default: rahmt execute mit Hooks
fn run(&mut self) {
self.on_start();
self.execute();
self.on_finish();
}
}
struct DebugWorker;
impl Worker for DebugWorker {
fn execute(&mut self) {
println!("Arbeit erledigt");
}
fn on_start(&mut self) {
println!(">>> Start");
}
// on_finish nicht überschrieben — bleibt no-op
}Leere Default-Methoden als optionale Hooks. Konsumenten können sie überschreiben, wenn sie wollen; sonst passiert nichts. Klassisches Pattern für Event-orientierte APIs.
FAQ
Default-Methode = Methode mit Body im Trait.
Im Trait wird nicht nur die Signatur, sondern auch der Body angegeben. Implementierende Typen müssen sie nicht selbst liefern — der Default wird verwendet, sofern kein Override existiert.
Defaults überschreiben mit normaler impl-Methode.
Im impl Trait for Typ-Block schreibst du die Methode einfach erneut. Der Compiler bevorzugt automatisch die spezialisierte Variante.
Defaults dürfen andere Trait-Methoden aufrufen.
Sowohl Pflicht- als auch andere Default-Methoden. Das ist das Template-Method-Pattern: aus wenigen Kern-Methoden entsteht ein reiches API.
Iterator ist das Stdlib-Paradebeispiel.
Eine Pflicht-Methode (next), ~70 Default-Methoden. Jeder Iterator-Typ braucht nur next zu implementieren — der ganze Iterator-Adapter-Apparat kommt gratis dazu.
Defaults können where-Bounds haben.
Methoden-spezifische Constraints, die zusätzlich zur Trait-Definition gelten. Damit kannst du Defaults nur für bestimmte Item-Typen anbieten (z.B. where Self::Item: Display).
Defaults sollten sinnvolle Standards sein.
Wenn der Default „panic!" ist, ist die Methode eigentlich Pflicht. Defaults sollen das „good enough"-Verhalten geben — spezialisierte Typen können bei Bedarf überschreiben.
Override für Optimierung — sehr verbreitet.
len() als Default zählt durch Iteration (O(n)); spezialisierte Typen wie Vec überschreiben mit O(1). Solche Performance-Overrides sind ein zentrales Pattern.
Defaults sind statischer Code, nicht dynamische Vererbung.
Anders als in OOP wird der Default zur Compile-Zeit eingesetzt. Es gibt keine Vtable-Magie — der Compiler kennt die Default-Implementation und verwendet sie direkt, sofern kein Override existiert.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Default Implementations
- Rust Reference – Default Methods
- std::iter::Iterator – Default-Methoden