Tiefer Eintauchgang in Rust Traits: Vererbung, Komposition und Polymorphismus
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Was ist ein Trait?
In Rust ist ein Trait eine Möglichkeit, gemeinsames Verhalten zu definieren. Er ermöglicht es uns, Methoden zu spezifizieren, die ein Typ implementieren muss, wodurch Polymorphismus und Interface-Abstraktion ermöglicht werden.
Hier ist ein einfaches Beispiel, das einen Trait namens Printable
definiert, der eine Methode namens print
enthält:
trait Printable { fn print(&self); }
Definieren und Implementieren von Traits
Um einen Trait zu definieren, verwenden wir das Schlüsselwort trait
, gefolgt vom Trait-Namen und einem Paar geschweifter Klammern. Innerhalb der geschweiften Klammern definieren wir die Methoden, die der Trait enthält.
Um einen Trait zu implementieren, verwenden wir das Schlüsselwort impl
, gefolgt vom Trait-Namen, dem Schlüsselwort for
und dem Typ, für den wir den Trait implementieren. Innerhalb der geschweiften Klammern müssen wir Implementierungen für alle im Trait definierten Methoden bereitstellen.
Im Folgenden finden Sie ein Beispiel, das zeigt, wie der zuvor definierte Printable
-Trait für den Typ i32
implementiert wird:
impl Printable for i32 { fn print(&self) { println!("{}", self); } }
In diesem Beispiel haben wir den Printable
-Trait für den Typ i32
implementiert und eine einfache Implementierung der print
-Methode bereitgestellt.
Trait-Vererbung und -Komposition
Rust erlaubt es uns, bestehende Traits durch Vererbung und Komposition zu erweitern. Die Vererbung ermöglicht es uns, in einem neuen Trait definierte Methoden eines Eltern-Traits wiederzuverwenden, während die Komposition es uns ermöglicht, mehrere verschiedene Traits in einem neuen Trait zu verwenden.
Hier ist ein Beispiel, das zeigt, wie die Vererbung verwendet wird, um den Printable
-Trait zu erweitern:
trait PrintableWithLabel: Printable { fn print_with_label(&self, label: &str) { print!("{}: ", label); self.print(); } }
In diesem Beispiel definieren wir einen neuen Trait namens PrintableWithLabel
, der von dem Printable
-Trait erbt. Dies bedeutet, dass jeder Typ, der PrintableWithLabel
implementiert, auch Printable
implementieren muss. Zusätzlich stellen wir eine neue Methode, print_with_label
, bereit, die eine Beschriftung ausgibt, bevor der Wert ausgegeben wird.
Hier ist ein weiteres Beispiel, das zeigt, wie die Komposition verwendet wird, um einen neuen Trait zu definieren:
trait DisplayAndDebug: Display + Debug {}
In diesem Beispiel definieren wir einen neuen Trait DisplayAndDebug
, der aus zwei Traits aus der Standardbibliothek besteht: Display
und Debug
. Dies bedeutet, dass jeder Typ, der DisplayAndDebug
implementiert, auch sowohl Display
als auch Debug
implementieren muss.
Traits als Parameter und Rückgabewerte
Rust erlaubt es uns, Traits als Parameter und Rückgabewerte in Funktionssignaturen zu verwenden, wodurch unser Code generischer und flexibler wird.
Hier ist ein Beispiel, das zeigt, wie der PrintableWithLabel
-Trait als Funktionsparameter verwendet wird:
fn print_twice<T: PrintableWithLabel>(value: T) { value.print_with_label("First"); value.print_with_label("Second"); }
In diesem Beispiel definieren wir eine Funktion namens print_twice
, die einen generischen Parameter T
entgegennimmt. Der Parameter muss den PrintableWithLabel
-Trait implementieren. Innerhalb des Funktionskörpers rufen wir die Methode print_with_label
auf dem Parameter auf.
Hier ist ein Beispiel, das zeigt, wie ein Trait als Funktionsrückgabewert verwendet wird:
fn get_printable() -> impl Printable { 42 }
Allerdings ist fn get_printable() -> impl Printable { 42 }
inkorrekt, da 42
eine ganze Zahl ist und den Printable
-Trait nicht implementiert.
Der richtige Ansatz ist, einen Typ zurückzugeben, der den Printable
-Trait implementiert. Wenn wir beispielsweise Printable
für den Typ i32
implementieren, können wir Folgendes schreiben:
impl Printable for i32 { fn print(&self) { println!("{}", self); } } fn get_printable() -> impl Printable { 42 }
In diesem Beispiel implementieren wir den Printable
-Trait für den Typ i32
und stellen eine einfache Implementierung der print
-Methode bereit. Dann geben wir in der Funktion get_printable
einen i32
-Wert 42
zurück. Da der Typ i32
den Printable
-Trait implementiert, ist dieser Code korrekt.
Trait-Objekte und statische Dispatch
In Rust können wir Polymorphismus auf zwei Arten erreichen: statische Dispatch und dynamische Dispatch.
- Statische Dispatch wird durch Verwendung von Generics erreicht. Wenn wir generische Parameter verwenden, generiert der Compiler separaten Code für jeden möglichen Typ. Dies ermöglicht, dass die Funktionsaufrufe zur Kompilierzeit bestimmt werden.
- Dynamische Dispatch wird durch Verwendung von Trait-Objekten erreicht. Wenn wir Trait-Objekte verwenden, generiert der Compiler einen Allzweckcode, der jeden Typ verarbeiten kann, der den Trait implementiert. Dies ermöglicht, dass die Funktionsaufrufe zur Laufzeit bestimmt werden.
Hier ist ein Beispiel, das zeigt, wie sowohl statische als auch dynamische Dispatch verwendet werden:
fn print_static<T: Printable>(value: T) { value.print(); } fn print_dynamic(value: &dyn Printable) { value.print(); }
In diesem Beispiel:
print_static
verwendet einen generischen ParameterT
, der denPrintable
-Trait implementieren muss. Wenn diese Funktion aufgerufen wird, generiert der Compiler separaten Code für jeden Typ, der an sie übergeben wird (statische Dispatch).print_dynamic
verwendet ein Trait-Objekt (&dyn Printable
) als Parameter. Dies ermöglicht dynamische Dispatch, wodurch die Funktion jeden Typ verarbeiten kann, der denPrintable
-Trait implementiert.
Assoziierte Typen und generische Einschränkungen
In Rust können wir assoziierte Typen und generische Einschränkungen verwenden, um komplexere Traits zu definieren.
Assoziierte Typen
Assoziierte Typen ermöglichen es uns, einen Typ zu definieren, der mit einem bestimmten Trait assoziiert ist. Dies ist nützlich, um Methoden zu definieren, die von einem assoziierten Typ abhängen.
Hier ist ein Beispiel, das einen Trait namens Add
unter Verwendung eines assoziierten Typs definiert:
trait Add<RHS = Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; }
In diesem Beispiel:
- Wir definieren einen Trait namens
Add
. - Er enthält einen assoziierten Typ
Output
, der den Rückgabetyp deradd
-Methode darstellt. - Der generische Parameter
RHS
spezifiziert die rechte Seite der Additionsoperation und hat standardmäßig den WertSelf
.
Generische Einschränkungen
Generische Einschränkungen ermöglichen es uns, zu spezifizieren, dass ein generischer Parameter bestimmte Bedingungen erfüllen muss (z. B. einen bestimmten Trait implementieren).
Hier ist ein Beispiel, das zeigt, wie generische Einschränkungen in einem Trait namens SummableIterator
verwendet werden:
use std::iter::Sum; trait SummableIterator: Iterator where Self::Item: Sum, { fn sum(self) -> Self::Item { self.fold(Self::Item::zero(), |acc, x| acc + x) } }
In diesem Beispiel:
- Wir definieren einen Trait
SummableIterator
, der den Standard-TraitIterator
erweitert. - Wir verwenden eine generische Einschränkung (
where Self::Item: Sum
), um zu spezifizieren, dass der TypItem
des Iterators den TraitSum
implementieren muss. - Die Methode
sum
berechnet die Gesamtsumme aller Elemente im Iterator.
Beispiel: Implementieren von Polymorphismus mithilfe von Traits
Hier ist ein Beispiel, das zeigt, wie der PrintableWithLabel
-Trait verwendet wird, um Polymorphismus zu erreichen:
struct Circle { radius: f64, } impl Printable for Circle { fn print(&self) { println!("Circle with radius {}", self.radius); } } impl PrintableWithLabel for Circle {} struct Square { side: f64, } impl Printable for Square { fn print(&self) { println!("Square with side {}", self.side); } } impl PrintableWithLabel for Square {} fn main() { let shapes: Vec<Box<dyn PrintableWithLabel>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Square { side: 2.0 }), ]; for shape in shapes { shape.print_with_label("Shape"); } }
In diesem Beispiel:
- Wir definieren zwei Strukturen:
Circle
undSquare
. - Beide Strukturen implementieren die Traits
Printable
undPrintableWithLabel
. - In der Funktion
main
erstellen wir einen Vektorshapes
, der Trait-Objekte (Box<dyn PrintableWithLabel>
) speichert. - Wir iterieren über den Vektor
shapes
und rufenprint_with_label
für jede Form auf.
Da sowohl Circle
als auch Square
PrintableWithLabel
implementieren, können sie als Trait-Objekte in einem Vektor gespeichert werden. Wenn wir print_with_label
aufrufen, bestimmt der Compiler dynamisch, welche Methode basierend auf dem tatsächlichen Typ des Objekts aufgerufen werden soll.
Dies ist, wie Traits Polymorphismus in Rust ermöglichen. Ich hoffe, dieser Artikel hilft Ihnen, Traits besser zu verstehen.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Rust-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen-Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützen 6,94 Mio. Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ