Generische Typen und wichtige Anwendungen

Eines der zentralen Konzepte zum Vermeiden von Codeduplikation sind generische Typen. Beispielsweise kann man eine Funktion oder einen Typen generisch definieren anstatt für einen konkreten Typen wie f32 um diese auch mit f64 verwenden zu können. Der uns bereits bekannte Typ Vec hat einen generischen Parameter T der angibt, welchen Typ die Elemente des Vektors haben. Generische Typparameter schreibt man wie generische Liftime-Parameter in spitze Klammern. Dementsprechend ist Vec<f32> ein Vektor mit Elementen des Typs f32 und Vec<f64> ein Vektor mit Elementen des Typs f64. Auch die primitiven zusammengesetzten Typen Array [T; n] und Tupel (T1, T2) haben einen generischen Typparameter. Deren Typannotation verwendet also keine Spitze Klammern. Z.B. ist [u8; 5] ein 5-elementiger Array natürlicher Zahlen bis 255. Das Tupel (String, bool, f64) hat eine Zeichenkette als ersten Typ, einen Wahrheitswert als zweiten Typ und eine Gleitkommazahl als dritten Typ.

Im Folgenden wollen wir unser Point-Beispiel aus dem Grundlagenkapitel generalisieren.

struct Point {
    x: f64,
    y: f64
}
impl Point {
    fn squared_dist_to_0(&self) -> f64 {
        self.x * self.x + self.y * self.y
    }
}

fn main() {
    let v = Point{ x: 1.0, y: 1.0 };
    let subtraction = v.squared_dist_to_0();
}

Point und seine Methode length sind nur für den Typ f64 definiert. Ein f32-Point ergibt aber genauso Sinn. Alles zu duplizieren und nur f64 durch f32 zu ersetzen, ist keine elegante Option. Wir können einen generischen Typen deklarieren.

struct Point<T> {
    x: T,
    y: T,
}
impl<T> Point<T> {
    fn squared_dist_to_0(&self) -> T {
        self.x * self.x + self.y * self.y 
    }
}

fn main() {
    let v = Point{ x: 1.0, y: 1.0 };
    v.squared_dist_to_0();
}

Wenn wir den obigen Schnipsel ausführen wollen, beschwert sich der Compiler, der Typ T unterstütze weder Multiplikation noch Addition. Da wir wissen, dass f64 Multiplikation unterstützt, können wir eine spezielle Implementierung für f64 bereit stellen.

struct Point<T> {
    x: T,
    y: T,
}
impl Point<f64> {
    fn squared_dist_to_0(&self) -> f64 {
        self.x * self.x + self.y * self.y 
    }
}

fn main() {
    let v = Point{ x: 1.0, y: 1.0 };
    v.squared_dist_to_0();

    let v: Point<i32> = Point{ x: 0, y: 1 };
    // v.squared_dist_to_0(); existiert nicht
}

Das ist jedoch nur eine Verschiebung des Problems, denn für f32 müssten wir squared_dist_to_0 ebenfalls separat implementieren. Wir brauchen eine allgemeine Bedingung des generischen Typs, die nur diejenigen konkreten Typen erlaubt, die diese Bedingung erfüllen. Daher werden wir im nächsten Abschnitt über Traits sprechen.