Traits in Rust
Ein Trait (Eigenschaft) legt ein Verhalten fest, das ein Typ implementieren kann – ähnlich wie ein Interface in anderen Sprachen. Damit beschreibt Rust „dieser Typ kann X“. Und genau darauf beruhen die Trait Bounds, die du auf der Seite Generics gesehen hast.
Einen Trait definieren
Abschnitt betitelt „Einen Trait definieren“trait Speak { fn speak(&self) -> String;}Das bedeutet: Jeder Typ, der Speak implementiert, muss eine Methode speak bereitstellen, die einen String zurückgibt. Für sich allein ist ein Trait nur ein Versprechen. Er tut nichts, bis ihn ein Typ implementiert.
Einen Trait implementieren
Abschnitt betitelt „Einen Trait implementieren“trait Speak { fn speak(&self) -> String;}
struct Dog;struct Cat;
impl Speak for Dog { fn speak(&self) -> String { String::from("Wuff!") }}
impl Speak for Cat { fn speak(&self) -> String { String::from("Miau!") }}
fn main() { let dog = Dog; let cat = Cat;
println!("{}", dog.speak()); println!("{}", cat.speak());}Ausgabe:
Wuff!Miau!Dog und Cat haben nichts miteinander zu tun. Aber beide implementieren Speak, also kannst du bei beiden .speak() aufrufen. Das ist dasselbe Muster wie die impl-Blöcke auf der Seite Structs. impl Speak for Dog sagt einfach: „So erfüllt Dog den Trait Speak.“
Traits als Funktionsparameter
Abschnitt betitelt „Traits als Funktionsparameter“Jetzt kommt der eigentliche Gewinn: Du kannst eine Funktion schreiben, die jeden Typ annimmt, der einen bestimmten Trait implementiert – egal, welcher Typ es genau ist:
trait Speak { fn speak(&self) -> String;}
struct Dog;impl Speak for Dog { fn speak(&self) -> String { String::from("Wuff!") }}
fn announce(animal: &impl Speak) { println!("Das Tier sagt: {}", animal.speak());}
fn main() { let dog = Dog; announce(&dog);}animal: &impl Speak bedeutet: „irgendein Typ, der Speak implementiert – welcher, muss ich nicht wissen“. Fügst du einen Typ Bird hinzu, der ebenfalls Speak implementiert, funktioniert announce auch damit, ohne dass du etwas ändern musst.
Traits, die du schon benutzt hast
Abschnitt betitelt „Traits, die du schon benutzt hast“Erinnerst du dich an #[derive(Debug)] von der Seite Structs? Debug ist selbst ein Trait. derive erzeugt die Implementierung einfach automatisch, statt dass du sie von Hand schreibst. Weitere häufige Traits, denen du begegnen wirst: Clone (für .clone()), PartialEq (für ==) und PartialOrd (für < und > – den hast du auf der Seite zu Generics schon als Trait Bound gesehen).
Standardimplementierungen
Abschnitt betitelt „Standardimplementierungen“Ein Trait kann einen Standard-Methodenrumpf mitbringen. Typen, die den Trait implementieren, können ihn so übernehmen oder überschreiben:
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Hallo, {}!", self.name()) }}Jeder Typ, der Greet implementiert, muss nur name() definieren. greet() bekommt er geschenkt – es sei denn, er möchte auch diese Methode überschreiben.
Zu Traits gibt es noch mehr, als diese Seite zeigt. Die Seite Häufige Traits stellt dir die Standard-Traits vor, die du jeden Tag benutzen wirst. Und Fortgeschrittene Traits behandelt assoziierte Typen, Supertraits und weitere Werkzeuge, die dir in größeren Codebasen begegnen.
Übungen
Abschnitt betitelt „Übungen“1. Formen mit einer Fläche
Abschnitt betitelt „1. Formen mit einer Fläche“Definiere einen Trait Area mit einer Methode fn area(&self) -> f64. Implementiere ihn für ein Rectangle (mit width und height) und ein Square (mit side). Gib die Fläche eines 3 mal 4 großen Rechtecks und eines Quadrats mit Seitenlänge 5 aus. Tipp: Jeder impl Area for ...-Block braucht nur diese eine Methode, und ihr Rumpf ist eine einzige Multiplikation.
Lösung anzeigen
trait Area { fn area(&self) -> f64;}
struct Rectangle { width: f64, height: f64,}
struct Square { side: f64,}
impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
impl Area for Square { fn area(&self) -> f64 { self.side * self.side }}
fn main() { let rect = Rectangle { width: 3.0, height: 4.0 }; let square = Square { side: 5.0 };
println!("Rechteck: {}", rect.area()); println!("Quadrat: {}", square.area());}Ausgabe:
Rechteck: 12Quadrat: 252. Eine Funktion für viele Typen
Abschnitt betitelt „2. Eine Funktion für viele Typen“Definiere einen Trait Describe mit fn describe(&self) -> String. Implementiere ihn für ein Book (mit einem title) und einen Movie (mit title und minutes). Schreib dann eine einzige Funktion fn show(item: &impl Describe), die die Beschreibung ausgibt, und ruf sie mit beiden auf. Tipp: Baue jede Beschreibung mit format! zusammen.
Lösung anzeigen
trait Describe { fn describe(&self) -> String;}
struct Book { title: String,}
struct Movie { title: String, minutes: u32,}
impl Describe for Book { fn describe(&self) -> String { format!("Buch: {}", self.title) }}
impl Describe for Movie { fn describe(&self) -> String { format!("Film: {} ({} Min.)", self.title, self.minutes) }}
fn show(item: &impl Describe) { println!("{}", item.describe());}
fn main() { let book = Book { title: String::from("Der Hobbit") }; let movie = Movie { title: String::from("Oben"), minutes: 96 };
show(&book); show(&movie);}Ausgabe:
Buch: Der HobbitFilm: Oben (96 Min.)3. Einen Standard übernehmen oder überschreiben
Abschnitt betitelt „3. Einen Standard übernehmen oder überschreiben“Nimm den Trait Greet von dieser Seite mit seiner Standardmethode greet(). Implementiere ihn für eine Person, die den Standard übernimmt, und für einen Pirate, der greet() überschreibt und stattdessen Ahoi, ...! sagt. Tipp: Beide Typen müssen name() definieren. Nur Pirate schreibt zusätzlich ein eigenes greet().
Lösung anzeigen
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Hallo, {}!", self.name()) }}
struct Person { name: String,}
struct Pirate { name: String,}
impl Greet for Person { fn name(&self) -> String { self.name.clone() }}
impl Greet for Pirate { fn name(&self) -> String { self.name.clone() }
fn greet(&self) -> String { format!("Ahoi, {}!", self.name()) }}
fn main() { let ana = Person { name: String::from("Ana") }; let jack = Pirate { name: String::from("Jack") };
println!("{}", ana.greet()); println!("{}", jack.greet());}Ausgabe:
Hallo, Ana!Ahoi, Jack!Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Ein Trait legt ein Verhalten fest, das ein Typ implementieren kann – ähnlich wie ein Interface.
impl TraitName for Typeliefert die Implementierung.&impl TraitNameals Parametertyp nimmt jeden Typ an, der diesen Trait implementiert.- Traits können Standard-Methodenrümpfe mitbringen, die Implementierer übernehmen oder überschreiben können.
Debug,CloneundPartialEqsind häufige Traits aus der Standardbibliothek –#[derive(...)]implementiert sie automatisch.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was legt ein Trait fest?
2. Was bedeutet animal: &impl Speak als Parametertyp?
3. Was macht #[derive(Debug)] eigentlich?
4. Was ist eine Standardimplementierung in einem Trait?
5. Welches davon ist laut dieser Seite ein Trait – und kein Struct oder Enum?
Punkte: 0 / 5
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Gemeinsames Verhalten definieren mit Traits – Das Rust-Buch: das offizielle Kapitel über das Definieren von Traits, Standardmethoden,
impl Trait-Parameter und Trait Bounds. - Traits – Rust by Example (auf Englisch): ein ausführbares Beispiel mit einem Trait, einer Standardmethode und einer Implementierung.
- Derive – Rust by Example (auf Englisch): welche Standard-Traits
#[derive(...)]für dich implementieren kann, mit Beispielen. - Fortgeschrittene Traits – Das Rust-Buch: der nächste Schritt, mit assoziierten Typen, Supertraits und dem Newtype-Muster.
- Traits – The Rust Reference (auf Englisch): die genauen Regeln für Trait-Definitionen, wenn du alle Details wissen willst.