Ein Supertrait ist ein Trait, das ein anderes Trait voraussetzt. Wenn trait Ord: PartialOrd deklariert ist, bedeutet das: jeder Typ, der Ord implementiert, muss auch PartialOrd implementieren. Damit baust du Trait-Hierarchien — schwächere Traits oben, stärkere Traits darunter, mit klaren Anforderungs-Ketten. Die Stdlib nutzt das ausgiebig: Copy: Clone, Eq: PartialEq, Ord: Eq + PartialOrd, Error: Debug + Display. Wer den Mechanismus versteht, kann saubere API-Hierarchien bauen, in denen jeder Trait die schwächere Variante als Basis nutzt.
Syntax
Ein Supertrait wird mit Doppelpunkt am Trait-Namen deklariert.
use std::fmt::Display;
// Sichtbar setzt Display voraus
trait Sichtbar: Display {
fn render(&self) -> String {
format!("[Sichtbar] {self}")
}
}
struct Punkt { x: i32, y: i32 }
// MUSS Display implementieren, weil Sichtbar es voraussetzt
impl Display for Punkt {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl Sichtbar for Punkt {}
fn main() {
let p = Punkt { x: 3, y: 4 };
println!("{}", p.render()); // "[Sichtbar] (3, 4)"
}trait Sichtbar: Display heißt: jeder Typ, der Sichtbar implementieren will, muss zuerst Display implementieren. Im Sichtbar-Body kannst du dann Display-Methoden (hier indirekt via format!("{self}")) nutzen, weil garantiert ist, dass self Display ist.
Wenn du impl Sichtbar for Punkt {} schreibst, ohne dass Punkt Display ist, gibt der Compiler einen Fehler:
error[E0277]: `Punkt` doesn't implement `std::fmt::Display`
note: required by a bound in `Sichtbar`
Mehrere Supertraits
Mit + kombinierst du mehrere Supertraits.
use std::fmt::{Debug, Display};
// Loggable setzt sowohl Debug als auch Display voraus
trait Loggable: Debug + Display {
fn log(&self) {
println!("DEBUG: {self:?}");
println!("DISPLAY: {self}");
}
}
struct Wert(i32);
impl Debug for Wert {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Wert({})", self.0)
}
}
impl Display for Wert {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Loggable for Wert {}
fn main() {
Wert(42).log();
// DEBUG: Wert(42)
// DISPLAY: 42
}Mehrere Supertraits sind sehr typisch — eine Trait-Hierarchie sammelt oft mehrere Voraussetzungen.
Supertrait-Methoden im Sub-Trait
Im Body des Sub-Traits kannst du Methoden des Supertraits via self aufrufen — sie sind garantiert verfügbar.
trait Counter {
fn count(&self) -> u32;
}
// ExtendedCounter setzt Counter voraus
trait ExtendedCounter: Counter {
// Default-Methode nutzt Counter::count
fn is_zero(&self) -> bool {
self.count() == 0
}
fn is_positive(&self) -> bool {
self.count() > 0
}
fn doubled(&self) -> u32 {
self.count() * 2
}
}
struct Counter5;
impl Counter for Counter5 {
fn count(&self) -> u32 { 5 }
}
// Automatisch: ExtendedCounter funktioniert, wenn Counter funktioniert
impl ExtendedCounter for Counter5 {}
fn main() {
let c = Counter5;
assert_eq!(c.count(), 5);
assert_eq!(c.doubled(), 10);
assert!(!c.is_zero());
assert!(c.is_positive());
}ExtendedCounter-Default-Methoden rufen self.count() auf — die Methode aus Counter. Der Compiler weiß, dass self Counter implementiert, weil ExtendedCounter es als Supertrait fordert.
Stdlib-Hierarchien
Die Stdlib hat einige sehr klare Supertrait-Beispiele.
// Klone-Hierarchie
// pub trait Copy: Clone {}
// → Copy verlangt Clone
// Vergleichs-Hierarchie
// pub trait Eq: PartialEq {}
// → Eq verlangt PartialEq
// pub trait Ord: Eq + PartialOrd {
// fn cmp(&self, other: &Self) -> Ordering;
// }
// → Ord verlangt Eq + PartialOrd
// Error-Hierarchie
// pub trait Error: Debug + Display { ... }
// → Error verlangt Debug + Display
// Iterator-Adapter mit Supertrait
// pub trait DoubleEndedIterator: Iterator {
// fn next_back(&mut self) -> Option<Self::Item>;
// }
// → DoubleEndedIterator verlangt IteratorJede dieser Hierarchien folgt einem Muster: schwächere Garantie oben, stärkere unten.
PartialEq— manche Werte sind vergleichbar (NaN ist kein Wert vergleichbar mit sich selbst)Eq— alle Werte sind vergleichbar (Reflexivität gilt)PartialOrd— Werte haben Reihenfolge, aber nicht alle PaareOrd— Total-Ordnung über alle Wert-Paare
Die stärkere Garantie verlangt die schwächere als Voraussetzung — das ist semantisch sinnvoll. Ein Typ kann nicht "total geordnet" sein, ohne "partiell geordnet" zu sein.
Supertrait ≠ OOP-Vererbung
Auch wenn die Syntax an Vererbung erinnert, sind Supertraits keine Vererbung:
trait Tier {
fn name(&self) -> &str;
}
// Hund ist KEIN Subtyp von Tier — es ist ein eigener Typ
// mit einem zusätzlichen Trait
trait Hund: Tier {
fn rasse(&self) -> &str;
}
struct Bello { rasse_name: String }
impl Tier for Bello {
fn name(&self) -> &str { "Bello" }
}
impl Hund for Bello {
fn rasse(&self) -> &str { &self.rasse_name }
}
// KEINE Subtyp-Beziehung: Bello hat Tier UND Hund implementiert,
// aber Bello ist nicht "abgeleitet von" irgendwas.Was Supertraits leisten:
- Voraussetzungs-Beziehung zwischen Traits
- Garantie für den Sub-Trait, dass Supertrait-Methoden verfügbar sind
- Logische Hierarchie der Anforderungen
Was sie nicht leisten:
- Keine "is-a"-Beziehung wie in OOP
- Kein gemeinsamer Speicher-Layout
- Keine automatische Vererbung von Method-Bodies (außer expliziten Default-Methoden)
- Kein Polymorphismus über Typen (das machen
dyn Traitund Generics)
Supertraits in Trait-Objekten
Wenn ein Trait dyn Trait werden soll und einen Supertrait hat, ist die Sache subtil: &dyn Sub kann nicht automatisch in &dyn Super konvertiert werden, auch wenn jeder Sub-Typ auch Super implementiert.
trait Tier {
fn name(&self) -> String;
}
trait Hund: Tier {
fn bellen(&self);
}
struct Bello;
impl Tier for Bello {
fn name(&self) -> String { String::from("Bello") }
}
impl Hund for Bello {
fn bellen(&self) { println!("Wuff!"); }
}
fn main() {
let bello = Bello;
// OK — direkt in &dyn Hund
let hund_obj: &dyn Hund = &bello;
hund_obj.bellen();
// hund_obj.name(); // OK in neueren Rust-Versionen (Supertrait-Upcasting)
// Direkter Tier-View
let tier_obj: &dyn Tier = &bello;
println!("{}", tier_obj.name());
}Stand 2026 (Rust 1.86+): Supertrait-Upcasting ist stabilisiert — &dyn Hund kann zu &dyn Tier gecasted werden, weil die Vtable die Supertrait-Methoden mitführt. In älteren Rust-Versionen musste man manuelle Wrapper bauen.
Praxis: Supertraits im echten Code
Hierarchische Validatoren
pub trait Validator {
fn validate(&self, input: &str) -> bool;
}
pub trait DetailedValidator: Validator {
fn validate_with_reason(&self, input: &str) -> Result<(), String>;
// Default: Default-Validate aus dem Result ableiten
fn validate(&self, input: &str) -> bool {
self.validate_with_reason(input).is_ok()
}
}
struct LengthValidator { min: usize, max: usize }
impl Validator for LengthValidator {
fn validate(&self, input: &str) -> bool {
let len = input.len();
len >= self.min && len <= self.max
}
}
impl DetailedValidator for LengthValidator {
fn validate_with_reason(&self, input: &str) -> Result<(), String> {
let len = input.len();
if len < self.min {
Err(format!("zu kurz: {len} < {}", self.min))
} else if len > self.max {
Err(format!("zu lang: {len} > {}", self.max))
} else {
Ok(())
}
}
}Zwei Trait-Ebenen: Validator mit einfacher Bool-Antwort, DetailedValidator mit detaillierter Fehler-Erklärung. Der Sub-Trait fordert den Super-Trait und erweitert die API.
Persistierbar mit Display-Anforderung
use std::fmt::Display;
pub trait Persistierbar: Display {
fn save_to(&self, path: &str) -> std::io::Result<()> {
std::fs::write(path, format!("{self}"))
}
}
struct Config { value: String }
impl Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "value={}", self.value)
}
}
impl Persistierbar for Config {}
fn main() -> std::io::Result<()> {
let c = Config { value: String::from("hello") };
c.save_to("/tmp/config.txt")?;
Ok(())
}Persistierbar setzt Display voraus, um Werte als String darzustellen. Implementierer brauchen nur Display zu liefern — die Save-Logik kommt gratis.
Geometrie-Hierarchie
pub trait Form {
fn flaeche(&self) -> f64;
}
pub trait FormMitUmfang: Form {
fn umfang(&self) -> f64;
fn flaeche_zu_umfang(&self) -> f64 {
self.flaeche() / self.umfang()
}
}
struct Kreis { radius: f64 }
impl Form for Kreis {
fn flaeche(&self) -> f64 {
std::f64::consts::PI * self.radius.powi(2)
}
}
impl FormMitUmfang for Kreis {
fn umfang(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
}Trait-Hierarchie für geometrische Formen: alle Formen haben Fläche, manche haben zusätzlich Umfang. Default-Methode nutzt beide Operationen.
Error-Hierarchie
use std::fmt::{self, Debug, Display};
pub trait MyError: Debug + Display {
fn code(&self) -> u32;
fn cause(&self) -> Option<&dyn MyError> { None }
}
#[derive(Debug)]
struct NotFoundError { resource: String }
impl Display for NotFoundError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Ressource nicht gefunden: {}", self.resource)
}
}
impl MyError for NotFoundError {
fn code(&self) -> u32 { 404 }
}Custom-Error-Trait mit Debug + Display als Supertraits — identisch zur Stdlib-Konvention für std::error::Error.
Service mit Cancel
pub trait Service {
fn execute(&mut self);
}
pub trait CancelableService: Service {
fn cancel(&mut self);
fn execute_with_timeout(&mut self, timeout_ms: u64) {
println!("Start service (timeout {timeout_ms}ms)");
self.execute();
// In Real-World: timer-Logik mit potentiellem cancel()
}
}
struct DataService { running: bool }
impl Service for DataService {
fn execute(&mut self) {
self.running = true;
println!("Daten werden verarbeitet");
}
}
impl CancelableService for DataService {
fn cancel(&mut self) {
self.running = false;
println!("Service abgebrochen");
}
}CancelableService erweitert Service um eine Cancel-Operation. Die Sub-Trait-Default execute_with_timeout kombiniert beide Methoden.
Lifecycle-Hierarchie
pub trait Startable {
fn start(&mut self);
}
pub trait Stoppable {
fn stop(&mut self);
}
// Restartable setzt BEIDE voraus
pub trait Restartable: Startable + Stoppable {
fn restart(&mut self) {
self.stop();
self.start();
}
}
struct Server { running: bool }
impl Startable for Server {
fn start(&mut self) {
self.running = true;
println!("Server gestartet");
}
}
impl Stoppable for Server {
fn stop(&mut self) {
self.running = false;
println!("Server gestoppt");
}
}
impl Restartable for Server {}Restartable kombiniert Startable und Stoppable als Supertraits und bietet eine Default-Methode, die beide nutzt. Klassisches Pattern für Lifecycle-orientierte Komponenten.
Iterator-Erweiterung
// Custom-Trait, der Iterator erweitert
pub trait CountingIterator: Iterator {
fn count_and_collect(self) -> (usize, Vec<Self::Item>)
where Self: Sized
{
let v: Vec<Self::Item> = self.collect();
let c = v.len();
(c, v)
}
}
// Blanket-Implementation: alles, was Iterator ist, ist auch CountingIterator
impl<I: Iterator> CountingIterator for I {}
fn main() {
let (c, v) = (1..=5).count_and_collect();
assert_eq!(c, 5);
assert_eq!(v, vec![1, 2, 3, 4, 5]);
}Trait-Extension-Pattern: ein Supertrait-basierter Sub-Trait, plus Blanket-Impl für alle Iteratoren. So fügst du jedem Iterator-Typ neue Methoden hinzu, ohne die Stdlib zu ändern.
Interessantes
Supertrait = vorausgesetzter Trait.
trait Sub: Super heißt: jeder Typ, der Sub implementiert, muss zuerst Super implementieren. Garantie für den Compiler und für Sub-Methoden, die Super-Methoden nutzen.
Mehrere Supertraits — kombiniert mit +.
trait Foo: Bar + Baz setzt sowohl Bar als auch Baz voraus. Im Foo-Body sind alle Methoden von Bar UND Baz auf self verfügbar.
Stdlib-Hierarchien — semantisch sinnvoll.
Copy: Clone, Eq: PartialEq, Ord: Eq + PartialOrd, Error: Debug + Display. Stärkere Garantien bauen auf schwächeren auf.
Kein OOP-Vererbungs-Konzept.
Supertraits sind Anforderungs-Ketten, keine Subtyp-Beziehungen. Es gibt keine geteilten Speicher-Layouts, keine automatische Method-Vererbung außer expliziten Default-Methoden.
Sub-Trait-Default-Methoden dürfen Super-Methoden aufrufen.
Im Sub-Trait-Body ist garantiert, dass self alle Super-Methoden hat. Damit kannst du in Defaults beliebig die Super-Methoden kombinieren.
Supertrait-Upcasting für dyn (ab Rust 1.86).
&dyn Sub lässt sich seit dieser Version automatisch zu &dyn Super casten. Die Vtable führt Supertrait-Methoden mit. In älteren Versionen mussten Wrapper-Methoden manuell gebaut werden.
Mehrere Supertraits — typisches Pattern.
Ein Trait mit zwei oder drei Supertraits ist nichts Ungewöhnliches. Schau Ord (drei: Eq, PartialOrd, plus implizit PartialEq) — saubere Trennung der Anforderungen.
Blanket-Impl + Supertrait = Trait-Extension-Pattern.
impl<I: Iterator> MyExtension for I {} fügt allen Iteratoren neue Methoden hinzu. Bekannte Stdlib-Variante: IteratorExt-Crates.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Using Supertraits
- Rust Reference – Supertraits
- Rust 1.86 – Trait Upcasting Stabilized