EMZETT.
Login

Kurz: Ein Trait beschreibt Verhalten, das Typen anbieten können (wie ein Interface):

Teil des Kurses Rust

Kapitel 9 von 13 im Kurs Rust. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

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());
    }
}

Ausgabe:

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]));
}

Ausgabe:

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));
}

Ausgabe:

3.14
9.00
7.14
impl Trait / Genericsdyn Trait
Entscheidungzur Übersetzungszeitzur Laufzeit
Geschwindigkeitschnellstmöglichkleiner Aufschlag
Verschiedene Typen in einer Listeneinja

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());
}

Ausgabe:

Paar { erstes: "Alter", zweites: 17 } Paar { erstes: 17, zweites: "Alter" }
Some(2) Some(1) None

Option<T> und Result<T, E> sind selbst generische Enums.

Wichtige Standard-Traits

TraitZwecktypisch mit #[derive]
Debug, DisplayAusgabe ({:?}, {})Debug
Clone, CopyKopierenja
PartialEq, Eq, PartialOrd, OrdVergleichen, Sortierenja
Hashals Schlüssel in HashMapja
DefaultStandardwert (Typ::default())ja
From, Into, TryFromUmwandlungenmanuell
IteratorFolgen erzeugenmanuell
DropAufräumen beim Freigebenmanuell
Add, Mul, Index …Operatoren überladenmanuell
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");
}

Ausgabe:

Vektor { x: 4.0, y: 6.0 } Vektor { x: 0.0, y: 0.0 }
Ende von main
schließe daten.txt

Drop ist Rusts Destruktor: Er läuft automatisch, wenn der Besitzer endet (RAII).

Merke

  • Traits beschreiben Verhalten; impl Trait for Typ setzt es um, Standardmethoden sind möglich
  • Generics mit Schranken (T: Display, where) werden zur Übersetzungszeit aufgelöst
  • impl Trait (statisch) und Box<dyn Trait> (dynamisch) für unterschiedliche Fälle
  • #[derive(Debug, Clone, PartialEq, Default)] erledigt Standardfälle
  • Drop, From, Add sind wichtige Standard-Traits

Übungsaufgabe

Definiere einen Trait Zahlung mit betrag(&self) -> f64 und zwei Typen, die ihn umsetzen. Summiere die Beträge einer Vec<Box<dyn Zahlung>>.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Vec, HashMap und andere Collections

Weiter: Iteratoren und Closures im Einsatz

Alle Kapitel: Rust im Überblick