Zum Inhalt springen

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.

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.

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!
▶ Selbst ausprobieren

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.“

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);
}
▶ Selbst ausprobieren

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.

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).

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.

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: 12
Quadrat: 25
▶ Selbst ausprobieren

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 Hobbit
Film: Oben (96 Min.)
▶ Selbst ausprobieren

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!
▶ Selbst ausprobieren
  • Ein Trait legt ein Verhalten fest, das ein Typ implementieren kann – ähnlich wie ein Interface.
  • impl TraitName for Type liefert die Implementierung.
  • &impl TraitName als Parametertyp nimmt jeden Typ an, der diesen Trait implementiert.
  • Traits können Standard-Methodenrümpfe mitbringen, die Implementierer übernehmen oder überschreiben können.
  • Debug, Clone und PartialEq sind häufige Traits aus der Standardbibliothek – #[derive(...)] implementiert sie automatisch.

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