Fortgeschrittene Traits in Rust
Auf der Seite Traits hast du gelernt, wie du einen Trait (Eigenschaft) definierst und implementierst. Auf den Seiten Generics und Wichtige Traits hast du Traits wie Display, From und Add benutzt. Für den meisten Alltagscode reicht das. Aber wenn du in die Standardbibliothek oder ein bekanntes Crate schaust, begegnen dir Trait-Features, die anfangs etwas seltsam aussehen: type Item, Add<Rhs = Self>, trait Pretty: Display, impl<T: Display> ... for T.
Diese Seite erklärt diese Features eins nach dem anderen. Keines davon ist Zauberei. Jedes löst ein kleines, echtes Problem – und sobald du das Problem kennst, ergibt auch die Syntax Sinn. Lass dir Zeit. Es ist völlig in Ordnung, einen Abschnitt zu lesen, den Code auszuprobieren und später wiederzukommen.
Assoziierte Typen: eine Antwort pro Typ
Abschnitt betitelt „Assoziierte Typen: eine Antwort pro Typ“Einen assoziierten Typ kennst du schon: Iterator hat type Item. Ein assoziierter Typ (Associated Type) ist ein Platzhalter-Typ in einem Trait. Jede Implementierung legt ihn genau einmal fest.
Hier ist ein kleiner Trait Container. Eine Playlist enthält Strings, eine Punkteliste enthält u32s – also wählt jeder Typ sein eigenes Item:
use std::fmt::Debug;
trait Container { type Item;
fn get(&self, index: usize) -> Option<&Self::Item>; fn size(&self) -> usize;
fn first(&self) -> Option<&Self::Item> { self.get(0) }}
struct Playlist { songs: Vec<String>,}
struct Scores { values: Vec<u32>,}
impl Container for Playlist { type Item = String;
fn get(&self, index: usize) -> Option<&String> { self.songs.get(index) } fn size(&self) -> usize { self.songs.len() }}
impl Container for Scores { type Item = u32;
fn get(&self, index: usize) -> Option<&u32> { self.values.get(index) } fn size(&self) -> usize { self.values.len() }}
// Funktioniert mit jedem Container, dessen Elemente sich mit {:?} ausgeben lassenfn describe<C: Container>(c: &C)where C::Item: Debug,{ println!("{} Elemente, das erste ist {:?}", c.size(), c.first());}
fn main() { let playlist = Playlist { songs: vec![String::from("Intro"), String::from("Outro")] }; let scores = Scores { values: vec![90, 72, 85] };
describe(&playlist); describe(&scores);}Ausgabe:
2 Elemente, das erste ist Some("Intro")3 Elemente, das erste ist Some(90)Im Trait bedeutet Self::Item: „der Typ, den diese Implementierung gewählt hat“. C::Item in describe bedeutet dasselbe für den Typ C. (Die where-Zeile ist nur ein aufgeräumterer Ort für die Bedingung – mehr dazu am Ende dieser Seite.)
Warum nicht einfach ein generischer Parameter? Du könntest trait Container<T> schreiben. Dann könnte aber ein Typ den Trait mehrfach implementieren – Container<String> und Container<u32> –, und jeder Aufrufer müsste angeben, welche Variante er meint. Für eine Playlist ergibt das keinen Sinn: Sie enthält Songs, Punkt.
Genau deshalb benutzt Iterator den assoziierten Typ type Item. Ein Iterator über einen Vec<i32> liefert i32 und nichts anderes. Deshalb braucht for x in v.iter() nie eine Typangabe. Eine einfache Faustregel:
- Eine natürliche Antwort pro Typ → assoziierter Typ (
Iterator::Item,Add::Output). - Viele gültige Antworten pro Typ → generischer Parameter (
From<T>: EinStringlässt sich aus einem&str, einemchar, einerBox<str>… erzeugen).
Standard-Typparameter und Operatoren
Abschnitt betitelt „Standard-Typparameter und Operatoren“Auf der Seite Wichtige Traits hast du + für einen Point zum Laufen gebracht, indem du Add implementiert hast. So ist Add in der Standardbibliothek tatsächlich definiert:
trait Add<Rhs = Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output;}Hier treffen zwei Features aufeinander. Output ist ein assoziierter Typ (das, was + liefert). Und Rhs = Self ist ein Standard-Typparameter (Default Type Parameter): „Die rechte Seite hat denselben Typ wie die linke, außer du sagst etwas anderes.“ Schreibst du impl Add for Point, bekommst du den Standard. Willst du einen anderen Typ addieren, schreibst du impl Add<OtherType> for Point.
use std::ops::Add;
#[derive(Debug, Clone, Copy)]struct Meters(f64);
#[derive(Debug, Clone, Copy)]struct Centimeters(f64);
// Nutzt den Standard Rhs = Self, also Meters + Metersimpl Add for Meters { type Output = Meters; fn add(self, other: Meters) -> Meters { Meters(self.0 + other.0) }}
// Überschreibt den Standard: Meters + Centimetersimpl Add<Centimeters> for Meters { type Output = Meters; fn add(self, other: Centimeters) -> Meters { Meters(self.0 + other.0 / 100.0) }}
fn main() { let a = Meters(2.0); let b = Meters(3.5); let c = Centimeters(50.0);
println!("{:?}", a + b); println!("{:?}", a + c);}Ausgabe:
Meters(5.5)Meters(2.5)Dir fällt sicher auf, dass Add beide Werkzeuge benutzt: einen generischen Parameter für die rechte Seite (viele gültige Antworten – du kannst Meters oder Centimeters addieren) und einen assoziierten Typ für das Ergebnis (eine Antwort pro Paar). Standard-Typparameter sind vor allem nützlich, wenn du selbst einen Trait entwirfst: Sie halten den häufigen Fall kurz, ohne den seltenen Fall zu verbauen.
Supertraits: „Das musst du auch implementieren“
Abschnitt betitelt „Supertraits: „Das musst du auch implementieren““Manchmal ergibt dein Trait nur Sinn, wenn der Typ schon etwas anderes kann. Ein Supertrait sagt: „Wer meinen Trait implementieren will, muss auch diesen anderen implementieren.“ Du schreibst ihn mit einem Doppelpunkt, wie eine Bedingung (Trait Bound):
use std::fmt;
// Alles, was Pretty ist, muss auch Display seintrait Pretty: fmt::Display { fn pretty(&self) -> String { format!("*** {} ***", self) }}
struct Temperature { celsius: f64,}
impl fmt::Display for Temperature { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:.1}°C", self.celsius) }}
impl Pretty for Temperature {}
fn main() { let t = Temperature { celsius: 21.456 }; println!("{}", t); println!("{}", t.pretty());}Ausgabe:
21.5°C*** 21.5°C ***Die Standardmethode pretty darf {} auf self anwenden, weil der Supertrait verspricht, dass Display vorhanden ist. Ohne : fmt::Display würde Rust dieses format! ablehnen – es könnte ja nicht wissen, ob sich der Typ ausgeben lässt. impl Pretty for Temperature {} ist leer. Es sagt nur: „Ja, nimm die Standardmethode.“
Echte Beispiele aus der Standardbibliothek: Copy: Clone (jeder Copy-Typ muss auch Clone sein) und Eq: PartialEq.
Blanket-Implementierungen: ein impl für viele Typen
Abschnitt betitelt „Blanket-Implementierungen: ein impl für viele Typen“Eine Blanket-Implementierung (Pauschal-Implementierung) implementiert einen Trait auf einen Schlag für jeden Typ, der eine Bedingung erfüllt. Die Syntax lautet impl<T: SomeBound> MyTrait for T:
use std::fmt::Display;
trait Shout { fn shout(&self) -> String;}
// Jeder Typ, der Display implementiert, bekommt Shout geschenktimpl<T: Display> Shout for T { fn shout(&self) -> String { format!("{}!", self.to_string().to_uppercase()) }}
fn main() { println!("{}", "hallo".shout()); println!("{}", 42.shout()); println!("{}", 'a'.shout()); println!("{}", String::from("rust ist toll").shout());}Ausgabe:
HALLO!42!A!RUST IST TOLL!Wir haben nie impl Shout for i32 oder impl Shout for char geschrieben. Die Blanket-Implementierung deckt sie alle ab – sogar Display-Typen, die es noch gar nicht gibt.
Du hast schon oft eine Blanket-Implementierung benutzt, ohne es zu wissen. Die Standardbibliothek enthält (grob) impl<T: Display> ToString for T. Deshalb hat jeder Typ mit Display eine Methode .to_string(). Derselbe Trick schenkt dir Into, wenn du From implementierst.
Eine Grenze gibt es: Sobald eine Blanket-Implementierung existiert, kannst du für einen Typ, den sie schon abdeckt, keine zweite, speziellere Implementierung hinzufügen (zum Beispiel ein eigenes impl Shout for i32). Rust hätte dann zwei Antworten und lehnt das mit einem Fehler „conflicting implementations“ ab.
Die Orphan Rule und der Newtype-Trick
Abschnitt betitelt „Die Orphan Rule und der Newtype-Trick“Diese Regel überrascht jeden einmal. Versuch, Vec<String> ein schönes Display zu geben:
use std::fmt;
impl fmt::Display for Vec<String> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.join(", ")) }}error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate --> src/main.rs:3:1 |3 | impl fmt::Display for Vec<String> { | ^^^^^^^^^^^^^^^^^^^^^^----------- | | | `Vec` is not defined in the current crate | = note: define and implement a trait or new type insteadDer Compiler sagt: Nur Traits aus deinem eigenen Crate dürfen für Typen von außerhalb implementiert werden. Das ist die Orphan Rule (Waisenregel): Du darfst einen Trait für einen Typ nur implementieren, wenn der Trait oder der Typ in deinem Crate definiert ist. Display gehört zur Standardbibliothek, Vec ebenfalls – keins von beiden gehört dir.
Warum gibt es diese Regel? Stell dir vor, zwei Crates, von denen du abhängst, hätten beide ihr eigenes impl Display for Vec<String> geschrieben. Welches soll Rust nehmen? Darauf gibt es keine gute Antwort, also macht Rust diese Situation gleich unmöglich.
Der Ausweg ist das Newtype-Muster: Du packst den fremden Typ in ein winziges eigenes Struct. Jetzt gehört der Typ dir, und du kannst für ihn implementieren, was du willst:
use std::fmt;
// Eine lokale Hülle um einen fremden Typstruct Shopping(Vec<String>);
impl fmt::Display for Shopping { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) }}
fn main() { let list = Shopping(vec![String::from("Milch"), String::from("Eier"), String::from("Brot")]); println!("Du brauchst: {}", list); println!("Das sind {} Sachen", list.0.len());}Ausgabe:
Du brauchst: [Milch, Eier, Brot]Das sind 3 SachenMit self.0 greifst du auf den Vec darin zu. Ein Newtype kostet zur Laufzeit nichts – im Speicher ist Shopping genau ein Vec<String>. Der Nachteil: Die Methoden von Vec stehen nicht direkt an Shopping zur Verfügung. Du schreibst also list.0.len() (oder ergänzt die Methoden, die du brauchst). Newtypes sind für viel mehr nützlich als nur für die Orphan Rule; mehr dazu auf der Seite Entwurfsmuster.
Vollständig qualifizierte Syntax: wenn Namen kollidieren
Abschnitt betitelt „Vollständig qualifizierte Syntax: wenn Namen kollidieren“Zwei Traits können Methoden mit demselben Namen haben – und der Typ selbst auch noch eine. Welche ruft robot.greet() dann auf?
trait English { fn greet(&self) -> String; fn language() -> String;}
trait Spanish { fn greet(&self) -> String; fn language() -> String;}
struct Robot;
impl Robot { fn greet(&self) -> String { String::from("PIEP PIEP") }}
impl English for Robot { fn greet(&self) -> String { String::from("Hello!") } fn language() -> String { String::from("Englisch") }}
impl Spanish for Robot { fn greet(&self) -> String { String::from("¡Hola!") } fn language() -> String { String::from("Spanisch") }}
fn main() { let r = Robot;
println!("{}", r.greet()); // die eigene Methode des Typs gewinnt println!("{}", English::greet(&r)); // einen Trait über seinen Namen wählen println!("{}", Spanish::greet(&r));
// Hier gibt es kein `self`, also braucht Rust den Typ UND den Trait: println!("{}", <Robot as English>::language()); println!("{}", <Robot as Spanish>::language());}Ausgabe:
PIEP PIEPHello!¡Hola!EnglischSpanischDrei Stufen, von kurz bis ganz ausführlich:
r.greet()– die eigene (inhärente) Methode des Typs wird zuerst gewählt.English::greet(&r)– „dieEnglish-Version“. Den Typ leitet Rust aus&rab.<Robot as English>::language()– die vollständig qualifizierte Syntax (Fully Qualified Syntax). Du brauchst sie für Funktionen ohneself, weil es keinen Wert gibt, an dem Rust den Typ ablesen könnte.English::language()allein schlägt fehl miterror[E0790]: cannot call associated function on trait without specifying the corresponding impl type– der Compiler weiß nicht, welche Implementierung du meinst.
Das brauchst du nicht oft. Und wenn doch, schlägt dir die Fehlermeldung meist schon die passende Lösung vor.
impl Trait in Parametern und Rückgabewerten, dazu where
Abschnitt betitelt „impl Trait in Parametern und Rückgabewerten, dazu where“Du kannst impl Trait an zwei Stellen schreiben, und es bedeutet jeweils etwas leicht anderes:
- Als Parametertyp –
fn f(x: impl Trait): Der Aufrufer wählt den Typ. Das ist eine Kurzform für die generische Funktionfn f<T: Trait>(x: T). - Als Rückgabetyp –
fn f() -> impl Trait: Die Funktion wählt einen konkreten Typ und hält ihn verborgen. Der Aufrufer weiß nur: „Es ist irgendetwas, dasTraitimplementiert.“
// Der Aufrufer wählt: Jeder Iterator über u32 ist rechtfn print_all(items: impl Iterator<Item = u32>) { let all: Vec<u32> = items.collect(); println!("{:?}", all);}
// Die Funktion wählt: Der echte Typ ist ein langes Filter<...>, das wir nie ausschreibenfn evens_up_to(limit: u32) -> impl Iterator<Item = u32> { (0..=limit).filter(|n| n % 2 == 0)}
fn main() { print_all(vec![3, 1, 4].into_iter()); print_all(evens_up_to(10));}Ausgabe:
[3, 1, 4][0, 2, 4, 6, 8, 10]impl Trait als Rückgabetyp ist besonders praktisch mit Closures und Iteratoren. Der echte Typ des Ergebnisses von evens_up_to enthält den Typ des Closures – und Closures haben keinen Namen, den du hinschreiben könntest. Mit impl Iterator musst du das auch nicht.
Hat eine Funktion mehrere generische Parameter mit jeweils mehreren Bedingungen, wird es in den spitzen Klammern schnell eng. Eine where-Klausel verschiebt die Bedingungen hinter die Signatur. So lässt sich die Form der Funktion leichter lesen:
use std::fmt::{Debug, Display};
// Gedrängt: fn report<T: Display, U: Debug + PartialOrd>(label: T, a: U, b: U) -> String// Aufgeräumter:fn report<T, U>(label: T, a: U, b: U) -> Stringwhere T: Display, U: Debug + PartialOrd,{ let bigger = if a > b { a } else { b }; format!("{}: größer ist {:?}", label, bigger)}
fn main() { println!("{}", report("Punkte", 7, 12)); println!("{}", report(String::from("Namen"), "ann", "bob"));}Ausgabe:
Punkte: größer ist 12Namen: größer ist "bob"Für den Compiler bedeuten beide Formen genau dasselbe. where ist auch der natürliche Ort für Bedingungen an assoziierte Typen, wie C::Item: Debug im ersten Beispiel dieser Seite.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“Einen Trait implementieren, aber seinen Supertrait vergessen. Entfernst du im Supertrait-Beispiel den Block impl fmt::Display for Temperature, bekommst du:
error[E0277]: `Temperature` doesn't implement `std::fmt::Display` --> src/main.rs:13:17 |13 | impl Pretty for Temperature {} | ^^^^^^^^^^^ unsatisfied trait bound |note: required by a bound in `Pretty` --> src/main.rs:3:15 | 3 | trait Pretty: fmt::Display { | ^^^^^^^^^^^^ required by this bound in `Pretty`Der Compiler meldet, dass Temperature den Trait Display nicht implementiert. Die Lösung: Implementiere den Supertrait (hier Display) ebenfalls. Der Hinweis („note“) zeigt genau auf das : fmt::Display, das ihn verlangt.
Zwei verschiedene Typen aus -> impl Trait zurückgeben. impl Trait verbirgt einen Typ. Es kann nicht „mal dieser, mal jener“ sein:
fn numbers(evens: bool) -> impl Iterator<Item = u32> { if evens { (0..10).filter(|n| n % 2 == 0) } else { vec![1, 3, 5].into_iter() }}error[E0308]: `if` and `else` have incompatible types --> src/main.rs:5:9 |2 | / if evens {3 | | (0..10).filter(|n| n % 2 == 0) | | ------------------------------ | | | | | | | the expected closure | | expected because of this4 | | } else {5 | | vec![1, 3, 5].into_iter() | | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Filter<Range<{integer}>, {closure@...}>`, found `IntoIter<{integer}>`6 | | } | |_____- `if` and `else` have incompatible types | = note: expected struct `Filter<std::ops::Range<{integer}>, {closure@src/main.rs:3:24: 3:27}>` found struct `std::vec::IntoIter<{integer}>`help: you could change the return type to be a boxed trait object |1 - fn numbers(evens: bool) -> impl Iterator<Item = u32> {1 + fn numbers(evens: bool) -> Box<dyn Iterator<Item = u32>> { |help: if you change the return type to expect trait objects, box the returned expressions |3 ~ Box::new((0..10).filter(|n| n % 2 == 0))4 | } else {5 ~ Box::new(vec![1, 3, 5].into_iter()) |Der Compiler sagt: if und else liefern unterschiedliche Typen. Wenn sich der Typ wirklich erst zur Laufzeit entscheidet, gib stattdessen eine Box<dyn Trait> zurück – genau wie der Compiler vorschlägt. Mehr dazu auf der Seite Trait-Objekte.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Ein assoziierter Typ (
type Item) liefert eine Antwort pro Implementierung. Nimm einen generischen Parameter, wenn ein Typ den Trait auf viele Arten implementieren kann. - Standard-Typparameter wie
Add<Rhs = Self>halten den häufigen Fall kurz;impl Add<Other> for Tüberschreibt sie. - Ein Supertrait (
trait Pretty: Display) verlangt einen weiteren Trait, und Standardmethoden dürfen ihn benutzen. - Eine Blanket-Implementierung (
impl<T: Display> MyTrait for T) deckt alle passenden Typen auf einmal ab – so funktioniert.to_string(). - Die Orphan Rule verhindert, dass du einen fremden Trait für einen fremden Typ implementierst. Pack den Typ in einen Newtype, um sie zu umgehen.
- Nimm die vollständig qualifizierte Syntax
<Type as Trait>::function(), wenn Namen kollidieren;impl Traitals Rückgabetyp verbirgt einen konkreten Typ;wheremacht lange Bedingungen lesbar.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Warum benutzt Iterator den assoziierten Typ type Item statt Iterator<T>?
2. Was bedeutet Rhs = Self in trait Add<Rhs = Self>?
3. Du willst impl Display for Vec<String> schreiben, aber Rust lehnt ab. Was ist die übliche Lösung?
4. Welche Zeile ruft die English-Version einer Funktion ohne self-Parameter auf?
5. Wer entscheidet bei fn evens() -> impl Iterator<Item = u32> über den konkreten Iterator-Typ?
Punkte: 0 / 5
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Fortgeschrittene Traits – Das Rust-Buch: assoziierte Typen, Standard-Typparameter, Supertraits und das Newtype-Muster, mit weiteren Beispielen.
- Traits – The Rust Reference (auf Englisch): die genauen Regeln für Trait-Elemente, Supertraits und assoziierte Elemente.
- Orphan rules – The Rust Reference (auf Englisch): die exakte Definition, wann eine Implementierung erlaubt ist.
impl Trait– The Rust Reference (auf Englisch): wie sichimpl Traitals Parametertyp und als Rückgabetyp unterscheiden.- Supertraits – Rust by Example (auf Englisch): ein kurzes, ausführbares Beispiel für Traits, die auf anderen Traits aufbauen.