Die Trait-Definition ist der Vertrag, den implementierende Typen erfüllen müssen. Sie besteht aus einer Sammlung von Methoden-Signaturen, optionalen Konstanten und Associated Types. Dieser Artikel zeigt die Syntax in voller Breite — von einfachen Single-Method-Traits bis zu komplexen Trait-Definitionen mit mehreren Methoden, Constants und generischen Method-Parametern. Wer einen Trait definiert, gibt die Form vor — implementierende Typen müssen sich an die Signaturen halten, der Compiler überprüft jede Stelle.
Die Grundsyntax
trait Drawable {
fn draw(&self);
}Drei Komponenten:
trait — das Schlüsselwort, das die Definition einleitet.
Name — in UpperCamelCase (Drawable, Iterator, Display). Die Rust-Konvention für Trait-Namen folgt typischerweise einem von zwei Patterns: Adjektive auf -able/-ible (Drawable, Comparable), oder Substantive, die eine Capability beschreiben (Iterator, Writer, Reader).
Block mit Methoden-Signaturen — Name, Parameter, Rückgabe-Typ, ohne Body. Jede Zeile endet mit ; statt mit einem Funktions-Block. Der Body wird vom implementierenden Typ geliefert.
Das &self als erster Parameter signalisiert: die Methode wird auf einer Instanz aufgerufen. Sie liest den State, ohne ihn zu verändern.
Self in Trait-Methoden
In Trait-Methoden gibt es drei Self-Receiver-Formen — analog zu Methoden in impl-Blöcken bei Structs.
trait Container {
fn lesen(&self) -> usize; // borrowt lesend
fn erweitern(&mut self); // borrowt mutierend
fn konsumieren(self); // verbraucht das Objekt
}&self ist der häufigste Receiver. Die Methode liest den State, verändert nichts. Aufrufer behalten Ownership, mehrere Borrows können parallel laufen. Default für die meisten Read-Operationen.
&mut self ist für mutierende Methoden. Die Bindung im Aufrufer muss mut sein, und während des Calls gibt es keinen anderen Borrow auf das Objekt. Für „verändere den State"-Operationen.
self verbraucht das Objekt. Nach dem Call existiert es nicht mehr. Klassisch für Konvertierungs-Methoden (into_string()), Builder-Abschluss (build()), oder Transformation, die einen anderen Typ zurückgibt.
Die Receiver-Wahl macht semantische Aussagen: &self sagt „ich brauche den Wert nur kurz und unverändert", &mut self sagt „ich werde ihn modifizieren", self sagt „ich übernehme ihn vollständig". Diese Signaturen sind Teil des Trait-Vertrags — Konsumenten sehen sofort, was die Methode mit ihrem Objekt macht.
Mehrere Methoden in einem Trait
trait Animal {
fn name(&self) -> &str;
fn sound(&self) -> String;
fn age(&self) -> u32;
fn introduce(&self) -> String { // Default-Methode (siehe nächster Artikel)
format!("Ich bin {}, sage {} und bin {} Jahre alt",
self.name(), self.sound(), self.age())
}
}Traits können beliebig viele Methoden enthalten. Sie können auch Default-Implementierungen haben (mehr im nächsten Artikel) — Methoden mit Body, die der implementierende Typ nicht zwingend überschreiben muss.
Bei Traits mit vielen Methoden lohnt sich Designdisziplin: ein Trait sollte eine kohärente Capability ausdrücken, nicht ein wahlloses Sammelsurium. Stdlib-Vorbilder: Iterator (~70 Methoden, alle um Sequenz-Verarbeitung), Read (~10 Methoden, alle um Byte-Lesen), Display (1 Methode, klare Aufgabe).
Wenn ein Trait fünf grundverschiedene Methoden enthält, ist es vermutlich zwei oder mehr Traits in einem — bessere Aufteilung in fokussierte Sub-Traits.
Associated Constants
Traits können auch Konstanten definieren, die jeder implementierende Typ liefern muss.
trait Bounded {
const MIN: i32;
const MAX: i32;
fn ist_im_bereich(&self, x: i32) -> bool {
x >= Self::MIN && x <= Self::MAX
}
}
struct Byte;
impl Bounded for Byte {
const MIN: i32 = 0;
const MAX: i32 = 255;
}
fn main() {
assert!(Byte.ist_im_bereich(100));
assert!(!Byte.ist_im_bereich(300));
println!("Byte-MAX: {}", Byte::MAX);
}const NAME: Type; in der Trait-Definition fordert, dass jeder implementierende Typ diesen Wert setzt. Im Trait-Body kannst du auf die Konstante über Self::CONST zugreifen.
Klassisches Stdlib-Beispiel: i32::MAX, i32::MIN, f64::EPSILON, f64::INFINITY — alles Associated Constants auf den jeweiligen Typen. Sie sind in Trait-Definitionen ebenso erlaubt.
Associated Functions ohne self
Eine Trait-Methode muss kein self-Parameter haben. Solche Methoden sind Associated Functions — sie gehören zum Typ, nicht zur Instanz.
trait Factory {
fn create() -> Self;
}
struct Widget { id: u64 }
impl Factory for Widget {
fn create() -> Self {
Widget { id: 42 }
}
}
fn main() {
let w = Widget::create();
assert_eq!(w.id, 42);
}fn create() -> Self ohne self-Parameter. Aufruf erfolgt mit Typ::methode() statt instanz.methode(). Klassisch für Konstruktor-artige Methoden — der Trait gibt vor, dass jeder Typ eine create-Function liefert, die eine Instanz erzeugt.
Bemerkenswert ist der Rückgabe-Typ Self: er steht für den konkreten Typ, der das Trait implementiert. Bei impl Factory for Widget wird aus Self automatisch Widget. So kann das Trait einen Rückgabe-Typ vorgeben, ohne ihn konkret zu nennen — jeder Typ liefert sich selbst zurück.
Generische Methoden in Traits
Trait-Methoden können selbst generisch sein.
use std::fmt::Display;
trait Logger {
fn log<T: Display>(&self, value: T);
}
struct StdLogger;
impl Logger for StdLogger {
fn log<T: Display>(&self, value: T) {
println!("[LOG] {value}");
}
}
fn main() {
let l = StdLogger;
l.log(42);
l.log("hello");
l.log(3.14);
}Die log-Methode ist generisch über T: Display. Konsumenten können sie mit beliebigen Display-fähigen Werten aufrufen — Strings, Zahlen, eigene Typen. Der implementierende Typ muss die generische Signatur exakt erfüllen.
Wichtig: Trait-Methoden mit generischen Parametern verhindern, dass das Trait als dyn Trait-Objekt verwendbar wird (Object-Safety-Problem, mehr im dyn-Artikel). Eine log<T: Display>(&self, value: T)-Methode wäre für jeden konkreten T monomorphisiert — eine Vtable kann das nicht repräsentieren.
Implementing für mehrere Typen
Ein Trait wird typischerweise für viele verschiedene Typen implementiert — das ist sein Sinn.
trait Area {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
struct Rectangle { width: f64, height: f64 }
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Area for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn drucke_area(form: &dyn Area) {
println!("{:.2}", form.area());
}
fn main() {
let c = Circle { radius: 5.0 };
let s = Square { side: 3.0 };
let r = Rectangle { width: 4.0, height: 6.0 };
drucke_area(&c); // 78.54
drucke_area(&s); // 9.00
drucke_area(&r); // 24.00
}Drei verschiedene Typen, ein gemeinsames Trait. Die drucke_area-Funktion arbeitet mit allen — sie kennt nur das Trait, nicht die konkreten Typen. Klassisches Polymorphismus-Pattern.
Diese Form kannst du beliebig erweitern: ein neuer Triangle-Typ braucht nur eine impl Area for Triangle-Block, und die drucke_area-Funktion funktioniert sofort mit ihm. Erweiterbarkeit ohne Änderung des bestehenden Codes — eines der Kernziele guten Trait-Designs.
Sichtbarkeit von Traits
Wie alle Items in Rust können Traits mit pub öffentlich gemacht werden.
// Public Trait — andere Crates können es implementieren und nutzen
pub trait Storable {
fn store(&self) -> Vec<u8>;
}
// Trait mit privater Methode
pub trait Foo {
fn pub_method(&self);
fn internal(&self); // Methode ist intern, aber muss von
// jedem Implementierer geliefert werden
}Bei einem pub trait sind alle Methoden automatisch öffentlich — du kannst nicht einzelne Methoden privat machen. Das Trait ist als Schnittstelle gedacht; eine private Methode darin wäre paradox.
Was du machen kannst: ein internes Trait definieren, das nur in deinem Crate sichtbar ist. Konsumenten sehen nur das öffentliche, das die internen Methoden nicht hat. Damit kannst du sehr feingranulare API-Designs bauen.
Trait-Definition vs. Trait-Implementation
Es ist wichtig, die zwei Konzepte sauber zu trennen.
Definition legt die Form fest: welche Methoden gibt es, welche Signaturen haben sie, welche Konstanten gehören dazu. Sie ist meist in einem anderen Modul oder Crate als die Implementation.
Implementation liefert die Body-Logik für einen konkreten Typ. Sie ist meist im selben Modul wie der Typ — also nahe an dessen Definition.
// In einem Library-Crate `geometry`:
pub mod geometry {
pub trait Area {
fn area(&self) -> f64;
}
}
// In einem anderen Crate, das `geometry` nutzt:
pub struct Circle { pub radius: f64 }
impl geometry::Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}Diese Trennung ist mächtig: das Library-Crate definiert das Vokabular (Area), Konsumenten-Crates implementieren es für ihre eigenen Typen. So entstehen Ökosysteme, in denen verschiedene Crates miteinander interagieren — etwa serde mit seinen Serialize/Deserialize-Traits, die unzählige Library-Typen implementieren.
Es gibt eine wichtige Einschränkung: die Orphan-Rule. Du kannst nicht ein fremdes Trait für einen fremden Typ implementieren. Mindestens eines davon muss aus deinem Crate stammen. Mehr dazu im eigenen Artikel.
Praxis: Trait-Definitionen im echten Code
Domain-Trait für Repository-Pattern
pub trait UserRepository {
fn finde_by_id(&self, id: u64) -> Option<User>;
fn speichere(&mut self, user: User) -> Result<(), String>;
fn loesche(&mut self, id: u64) -> bool;
fn alle(&self) -> Vec<User>;
}
#[derive(Clone)]
pub struct User { pub id: u64, pub name: String }Repository-Trait für Datenbank-Abstraktion. Implementierungen können verschiedene Backends nutzen — In-Memory für Tests, PostgreSQL für Production, REST-API für Services. Aus Konsumenten-Sicht ist es immer die gleiche Schnittstelle.
Logger-Trait mit verschiedenen Output-Channels
pub trait Log {
fn debug(&self, msg: &str);
fn info(&self, msg: &str);
fn warn(&self, msg: &str);
fn error(&self, msg: &str);
}
pub struct StdoutLogger;
pub struct FileLogger { pub pfad: String }
pub struct NullLogger;
impl Log for StdoutLogger {
fn debug(&self, msg: &str) { println!("[DEBUG] {msg}"); }
fn info(&self, msg: &str) { println!("[INFO] {msg}"); }
fn warn(&self, msg: &str) { eprintln!("[WARN] {msg}"); }
fn error(&self, msg: &str) { eprintln!("[ERROR] {msg}"); }
}
impl Log for NullLogger {
fn debug(&self, _: &str) {}
fn info(&self, _: &str) {}
fn warn(&self, _: &str) {}
fn error(&self, _: &str) {}
}Logging-Trait mit drei Levels. Verschiedene Implementierungen ermöglichen verschiedene Verhaltensweisen — NullLogger ist nützlich für Tests, wo die Log-Ausgabe nur stört.
Event-Handler-Trait
pub trait EventHandler {
type Event;
fn handle(&mut self, event: Self::Event);
fn priority(&self) -> u8 { 0 } // Default-Methode
}
// Konkrete Implementation:
struct ClickHandler;
impl EventHandler for ClickHandler {
type Event = (i32, i32); // x, y
fn handle(&mut self, event: Self::Event) {
let (x, y) = event;
println!("Click bei ({x}, {y})");
}
}Trait mit Associated Type (Event) und Default-Methode (priority). Beide Konzepte werden in eigenen Artikeln vertieft. Das Pattern zeigt, wie mächtig Traits werden, wenn alle Features kombiniert sind.
Validator-Trait
pub trait Validator {
type Input;
fn validate(&self, input: &Self::Input) -> Result<(), Vec<String>>;
}
pub struct EmailValidator;
impl Validator for EmailValidator {
type Input = String;
fn validate(&self, input: &String) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if !input.contains('@') {
errors.push("Fehlt @".into());
}
if input.is_empty() {
errors.push("Leer".into());
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
}Validator-Trait mit Associated Type für den Input. Verschiedene Validatoren können verschiedene Input-Typen prüfen, ohne dass das Trait selbst generisch über den Input ist.
Numerisches Konvertier-Trait
pub trait ToNumber {
fn to_f64(&self) -> f64;
fn to_i64(&self) -> i64 {
self.to_f64() as i64 // Default basierend auf f64
}
}
impl ToNumber for i32 {
fn to_f64(&self) -> f64 { *self as f64 }
}
impl ToNumber for &str {
fn to_f64(&self) -> f64 {
self.parse().unwrap_or(0.0)
}
}Eigenes Konvertier-Trait mit Stdlib-Typ-Implementierungen. Der Trait ist eigener Code, also kannst du ihn auch für Stdlib-Typen wie i32 und &str implementieren — die Orphan-Rule erlaubt das (eines von Trait oder Typ muss aus deinem Crate stammen, hier das Trait).
Serializable mit Default
pub trait Serializable {
fn to_json(&self) -> String;
// Default: ruft to_json und wandelt in Bytes
fn to_json_bytes(&self) -> Vec<u8> {
self.to_json().into_bytes()
}
// Default: nutzt to_json fuer Pretty-Print
fn to_pretty(&self) -> String {
let raw = self.to_json();
// Hier würde echte Pretty-Print-Logik stehen
format!("/* Pretty */\n{raw}")
}
}Trait mit einer Pflicht-Methode (to_json) und zwei Default-Methoden. Implementierende Typen müssen nur die Kern-Methode liefern; die Convenience-Methoden bekommen sie automatisch dazu. Klassisches Stdlib-Pattern.
Iterator-Style-Trait
pub trait Stream {
type Item;
fn next_item(&mut self) -> Option<Self::Item>;
// Default: zählt verbleibende Items
fn count(mut self) -> usize where Self: Sized {
let mut c = 0;
while self.next_item().is_some() {
c += 1;
}
c
}
// Default: sammelt in Vec
fn collect_all(mut self) -> Vec<Self::Item> where Self: Sized {
let mut v = Vec::new();
while let Some(item) = self.next_item() {
v.push(item);
}
v
}
}Iterator-artiges Trait mit where Self: Sized an einigen Default-Methoden. Die Sized-Constraint ist nötig, weil self (verbrauchend) nur bei Sized-Typen funktioniert — bei dyn Trait-Objekten wäre der Trait sonst nicht object-safe.
Interessantes
Trait = Vertrag.
Ein Trait definiert eine Sammlung von Methoden-Signaturen. Implementierende Typen müssen sie alle liefern. Der Compiler überprüft jede Stelle. Es gibt keine „optional"-Methoden — alles ist Pflicht (außer mit Default-Body, siehe nächster Artikel).
&self, &mut self, self sind die drei Receiver.
&self für lesende Methoden, &mut self für mutierende, self für konsumierende. Wahl gehört zur API-Design-Entscheidung — Konsumenten sehen am Signatur, was die Methode mit dem Objekt macht.
Methoden ohne self sind Associated Functions.
Aufruf mit Typ::funktion() statt objekt.methode(). Klassisch für Konstruktoren in Traits, etwa fn create() -> Self.
Self = der implementierende Typ.
In Trait-Definitionen ist Self ein Platzhalter für „der konkrete Typ, der dieses Trait implementiert". Bei fn neu() -> Self wird daraus beim Implementieren der jeweilige Typ.
Trait-Methoden können generisch sein.
fn log<T: Display>(&self, value: T) ist erlaubt. Aber: generische Methoden machen den Trait non-object-safe (kein dyn Trait möglich).
Associated Constants über const NAME: Type;.
Konstanten als Teil des Trait-Vertrags. Implementierende Typen müssen den Wert setzen. Im Trait-Body über Self::NAME zugreifbar.
Trait-Sichtbarkeit gilt für das ganze Trait.
Bei pub trait Foo sind alle Methoden öffentlich. Kein Mechanismus für „diese Methode ist privat innerhalb des Traits". Wer interne Methoden braucht, splittet in mehrere Traits.
Naming-Konvention: -able oder Capability.
Drawable, Comparable, Cloneable für Adjektiv-Form. Iterator, Reader, Writer für Substantiv-Form. Stdlib mischt beide, je nachdem was natürlicher klingt.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Defining a Trait
- Rust Reference – Traits
- Rust by Example – Traits
- Rust API Guidelines