Traits: gemeinsames Verhalten
Ein Trait beschreibt Verhalten, das Typen anbieten können (wie ein Interface):
trait Tier {
fn name(&self) -> String;
fn laut(&self) -> String;
fn vorstellen(&self) -> String { // Standardimplementierung
format!("{} macht {}", self.name(), self.laut())
}
}
struct Hund;
struct Katze { name: String }
impl Tier for Hund {
fn name(&self) -> String { "Hund".to_string() }
fn laut(&self) -> String { "Wuff".to_string() }
}
impl Tier for Katze {
fn name(&self) -> String { self.name.clone() }
fn laut(&self) -> String { "Miau".to_string() }
fn vorstellen(&self) -> String { format!("{} schnurrt und sagt {}", self.name, self.laut()) }
}
fn main() {
let tiere: Vec<Box<dyn Tier>> = vec![Box::new(Hund), Box::new(Katze { name: "Mimi".into() })];
for t in &tiere {
println!("{}", t.vorstellen());
}
}Hund macht Wuff Mimi schnurrt und sagt Miau
Box<dyn Tier> (ein Trait-Objekt) erlaubt Werte verschiedener Typen in einer Liste; welche Methode läuft, wird zur Laufzeit entschieden.
Generics mit Trait-Schranken
Generische Funktionen arbeiten mit vielen Typen. Mit Schranken (bounds) legst du fest, was ein Typ können muss. Die Auflösung passiert zur Übersetzungszeit (kein Laufzeitaufwand):
use std::fmt::Display;
fn groesser<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn zeige<T: Display>(wert: T) -> String {
format!("<{wert}>")
}
fn beschreibe<T>(liste: &[T]) -> String
where
T: Display + Clone, // Schranken übersichtlich mit where
{
liste.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(", ")
}
fn summe<T: std::iter::Sum<T> + Copy>(werte: &[T]) -> T {
werte.iter().copied().sum()
}
fn main() {
println!("{} {} {}", groesser(3, 9), groesser(2.5, 1.5), groesser("kiwi", "apfel"));
println!("{} {}", zeige(42), zeige("text"));
println!("{}", beschreibe(&[1, 2, 3]));
println!("{} {}", summe(&[1, 2, 3]), summe(&[1.5, 2.5]));
}9 2.5 kiwi <42> <text> 1, 2, 3 6 4
impl Trait
In Argumenten und Rückgaben kürzer:
trait Flaeche { fn flaeche(&self) -> f64; }
struct Quadrat(f64);
struct Kreis(f64);
impl Flaeche for Quadrat { fn flaeche(&self) -> f64 { self.0 * self.0 } }
impl Flaeche for Kreis { fn flaeche(&self) -> f64 { 3.14159 * self.0 * self.0 } }
fn drucke(f: &impl Flaeche) { println!("{:.2}", f.flaeche()); }
fn erzeuge() -> impl Flaeche { Quadrat(3.0) }
fn gesamt(formen: &[Box<dyn Flaeche>]) -> f64 { formen.iter().map(|f| f.flaeche()).sum() }
fn main() {
drucke(&Kreis(1.0));
drucke(&erzeuge());
let liste: Vec<Box<dyn Flaeche>> = vec![Box::new(Quadrat(2.0)), Box::new(Kreis(1.0))];
println!("{:.2}", gesamt(&liste));
}3.14 9.00 7.14
impl Trait / Generics | dyn Trait | |
|---|---|---|
| Entscheidung | zur Übersetzungszeit | zur Laufzeit |
| Geschwindigkeit | schnellstmöglich | kleiner Aufschlag |
| Verschiedene Typen in einer Liste | nein | ja |
Generische Structs und Enums
#[derive(Debug)]
struct Paar<A, B> {
erstes: A,
zweites: B,
}
impl<A: Clone, B: Clone> Paar<A, B> {
fn tauschen(&self) -> Paar<B, A> {
Paar { erstes: self.zweites.clone(), zweites: self.erstes.clone() }
}
}
struct Stapel<T> { daten: Vec<T> }
impl<T> Stapel<T> {
fn neu() -> Self { Stapel { daten: Vec::new() } }
fn push(&mut self, x: T) { self.daten.push(x); }
fn pop(&mut self) -> Option<T> { self.daten.pop() }
}
fn main() {
let p = Paar { erstes: "Alter", zweites: 17 };
println!("{:?} {:?}", p, p.tauschen());
let mut s = Stapel::neu();
s.push(1); s.push(2);
println!("{:?} {:?} {:?}", s.pop(), s.pop(), s.pop());
}Paar { erstes: "Alter", zweites: 17 } Paar { erstes: 17, zweites: "Alter" }
Some(2) Some(1) NoneOption<T> und Result<T, E> sind selbst generische Enums.
Wichtige Standard-Traits
| Trait | Zweck | typisch mit #[derive] |
|---|---|---|
Debug, Display | Ausgabe ({:?}, {}) | Debug |
Clone, Copy | Kopieren | ja |
PartialEq, Eq, PartialOrd, Ord | Vergleichen, Sortieren | ja |
Hash | als Schlüssel in HashMap | ja |
Default | Standardwert (Typ::default()) | ja |
From, Into, TryFrom | Umwandlungen | manuell |
Iterator | Folgen erzeugen | manuell |
Drop | Aufräumen beim Freigeben | manuell |
Add, Mul, Index ... | Operatoren überladen | manuell |
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
struct Vektor { x: f64, y: f64 }
impl Add for Vektor {
type Output = Vektor;
fn add(self, o: Vektor) -> Vektor { Vektor { x: self.x + o.x, y: self.y + o.y } }
}
impl From<(f64, f64)> for Vektor {
fn from((x, y): (f64, f64)) -> Self { Vektor { x, y } }
}
struct Datei(&'static str);
impl Drop for Datei {
fn drop(&mut self) { println!("schließe {}", self.0); }
}
fn main() {
let a = Vektor { x: 1.0, y: 2.0 };
let b: Vektor = (3.0, 4.0).into();
println!("{:?} {:?}", a + b, Vektor::default());
let _d = Datei("daten.txt");
println!("Ende von main");
}Vektor { x: 4.0, y: 6.0 } Vektor { x: 0.0, y: 0.0 }
Ende von main
schließe daten.txtDrop ist Rusts Destruktor: Er läuft automatisch, wenn der Besitzer endet (RAII).
Merke
- Traits beschreiben Verhalten;
impl Trait for Typsetzt es um, Standardmethoden sind möglich - Generics mit Schranken (
T: Display,where) werden zur Übersetzungszeit aufgelöst impl Trait(statisch) undBox<dyn Trait>(dynamisch) für unterschiedliche Fälle#[derive(Debug, Clone, PartialEq, Default)]erledigt StandardfälleDrop,From,Addsind wichtige Standard-Traits
Aufgabe
Definiere einen Trait Zahlung mit betrag(&self) -> f64 und zwei Typen, die ihn umsetzen. Summiere die Beträge einer Vec<Box<dyn Zahlung>>.