Trait Objekte

In Rust können wir Funktionalität für alle Implementierer eines Traits bereitstellen. Neben statischer Polymorphie mit generischen Datentypen können wir auch konkrete Typen und Trait Objekte verwenden, deren Gestalt erst zur Laufzeit fest steht. Dazu betrachten wir erneut unser Punkt-Kreis-Linien-Beispiel.

#![allow(unused)]
fn main() {
use std::ops::{Mul, Add, Sub};
trait Calculate: Mul<Output=Self> 
   + Add<Output=Self> 
   + Sub<Output=Self> 
   + Copy {}
impl<T: Mul<Output=Self> 
   + Add<Output=Self> 
   + Sub<Output=Self> 
   + Copy> Calculate for T {}
trait MeasureDistanceTo0<T: Calculate> {
   fn squared_dist_to_0(&self) -> T; 
}
fn longest_dist_to_0<T>(
    p1: &dyn MeasureDistanceTo0<T>, 
    p2: &dyn MeasureDistanceTo0<T>
) -> T
where
    T: Calculate + PartialOrd
{
    let d1 = p1.squared_dist_to_0();
    let d2 = p2.squared_dist_to_0();
    if  d1 > p2.squared_dist_to_0() {
        d1
    } else {
        d2
    }
}
}

Die generischen Parameter reduzieren sich zu T, das oft f32 oder f64 annimmt. Unser Trait MeasureDistanceTo0 ändert sich nicht unabhängig von seiner Nutzung in statischer oder dynamischer Polymorphie. Die Parameter der Funktion longest_dist_to_0 sind nun Referenzen auf dyn MeasureDistanceTo0<T>. Das Schlüsselwort dyn dient nur der Markierung von Trait-Objekten und war in alten Versionen von Rust optional. Es ist jedoch nicht Möglich dyn MeasureDistanceTo0<T> ohne Referenz als Argument zu verwenden, denn die Größe der Typen muss zur Kompilierzeit feststehen, wie wir im Kapitel über Heap und Stack gelernt haben. Wir haben nun eine weitere Alternative zur Erstellung von verschiedengestaltigen Containern.

use std::{cmp::Ordering, ops::{Mul, Add, Sub}};
trait Calculate: Mul<Output=Self> 
   + Add<Output=Self> 
   + Sub<Output=Self> 
   + Copy
   + From<i32> {}
impl<T: Mul<Output=Self> 
   + Add<Output=Self> 
   + Sub<Output=Self> 
   + Copy
   + From<i32>> Calculate for T {}
#[derive(Copy, Clone)]
struct Point<T>
where 
    T: Calculate
{
    x: T,
    y: T,
}
impl<T> MeasureDistanceTo0<T> for Point<T> 
where 
    T: Calculate 
{
    fn squared_dist_to_0(&self) -> T {
        self.x * self.x + self.y * self.y
    }
}
struct Circle<T: Calculate> {
   center: Point<T>,
   r: T
}
trait Sqrt {
    fn sqrt(self) -> Self;
}
impl Sqrt for f64 
{
    fn sqrt(self) -> f64 {
        self.sqrt()
    }
}
impl Sqrt for f32 
{
    fn sqrt(self) -> f32 {
        self.sqrt()
    }
}
impl<T> MeasureDistanceTo0<T> for Circle<T> 
where 
    T: Calculate + Sqrt
{
    fn squared_dist_to_0(&self) -> T {
        self.center.squared_dist_to_0().sqrt() - self.r
    }
}
trait MeasureDistanceTo0<T: Calculate> {
   fn squared_dist_to_0(&self) -> T; 
}
fn longest_dist_to_0<T>(ps: &[&dyn MeasureDistanceTo0<T>]) -> T
where
    T: Calculate + PartialOrd
{
    ps.iter().map(|p| p.squared_dist_to_0()).max_by(|a, b|{
        match a.partial_cmp(b) {
            Some(o) => o,
            None => Ordering::Equal,
        }
    }).unwrap_or(T::from(0))
}
fn main() {
    let point = Point{ x: 0.1, y:0.2 };
    let point = Point{ x: 0.1, y:0.2 };
    let circle = Circle{ center: point, r: 0.3 };
    let v: Vec<&dyn MeasureDistanceTo0<f64>> = vec![
        &point, 
        &point, 
        &circle
    ];
    let ld = longest_dist_to_0(&v);
    println!("{ld}");
}

Der innere Typ des Vektors v muss explizit als &dyn MeasureDistanceTo0 angegeben werden, da ansonsten der Compiler sich weigert, die Typen &Point und &Circle in einem Vektor unterzubringen.

Angenommen, wir möchten ein Trait-Objekt nicht per Referenz übergeben, sondern wir wollen Ownership abgeben. In diesem Fall können wir unser Trait-Objekt in einen Zeiger einwickeln. In Rust gibt es verschiedene Zeigertypen. Der wichtigste Zeiger heißt Box<T>. Ein Box<T>-Zeiger legt einen Wert von Typ T auf den Heap und behält auf dem Stack nur die Adresse. Des weiteren hat Box Ownership über den Wert und entsprechend eine drop-Implementierung, die den Wert aufräumt. Beispielsweise landet der in einem Box-Zeiger verpacktem Array

#![allow(unused)]
fn main() {
let x = Box::new([10, 20, 30]);
}

inklusive Metadaten auf dem Heap

Stack                              Heap

| address | name | value    |      | address | value |
| ------- | ---- | -------- |      | ------- | ----- |
|         |      |          |      | 0x2040  | ?     |
|         |      |          |      | 0x2036  | 30    |
|         |      |          |      | 0x2032  | 20    |
|         |      |          |      | 0x2028  | 10    |
| 0x1064  | x    | 0x2024   | ---> | 0x2024  | len   |
|         |      |          |      | 0x2020  | ?     |

während der Array [10, 20, 30] ohne Box-Verpackung komplett auf dem Stack landen würde. Wir können also anstatt Referenzen einen Box-Zeiger verwenden, der unabhängig von der Gestalt identischen Platz auf dem Stack einnimmt und die Gestalt besitzt anstatt sie nur zu referenzieren.

use std::{cmp::Ordering, ops::{Mul, Add, Sub}};
trait Calculate: Mul<Output=Self> 
   + Add<Output=Self> 
   + Sub<Output=Self> 
   + Copy
   + From<i32> {}
impl<T: Mul<Output=Self> 
   + Add<Output=Self> 
   + Sub<Output=Self> 
   + Copy
   + From<i32>> Calculate for T {}
#[derive(Copy, Clone)]
struct Point<T>
where 
    T: Calculate
{
    x: T,
    y: T,
}
impl<T> MeasureDistanceTo0<T> for Point<T> 
where 
    T: Calculate 
{
    fn squared_dist_to_0(&self) -> T {
        self.x * self.x + self.y * self.y
    }
}
struct Circle<T: Calculate> {
   center: Point<T>,
   r: T
}
trait Sqrt {
    fn sqrt(self) -> Self;
}
impl Sqrt for f64 
{
    fn sqrt(self) -> f64 {
        self.sqrt()
    }
}
impl Sqrt for f32 
{
    fn sqrt(self) -> f32 {
        self.sqrt()
    }
}
impl<T> MeasureDistanceTo0<T> for Circle<T> 
where 
    T: Calculate + Sqrt
{
    fn squared_dist_to_0(&self) -> T {
        self.center.squared_dist_to_0().sqrt() - self.r
    }
}
trait MeasureDistanceTo0<T: Calculate> {
   fn squared_dist_to_0(&self) -> T; 
}
fn longest_dist_to_0<T>(ps: &[Box<dyn MeasureDistanceTo0<T>>]) -> T
where
    T: Calculate + PartialOrd
{
    ps.iter().map(|p| p.squared_dist_to_0()).max_by(|a, b|{
        match a.partial_cmp(b) {
            Some(o) => o,
            None => Ordering::Equal,
        }
    }).unwrap_or(T::from(0))
}
fn main() {
    let point = Point{ x: 0.1, y:0.2 };
    let point = Point{ x: 0.1, y:0.2 };
    let circle = Circle{ center: point, r: 0.3 };
    let v: Vec<Box<dyn MeasureDistanceTo0<f64>>> = vec![
        Box::new(point), 
        Box::new(point), 
        Box::new(circle)
    ];
    let ld = longest_dist_to_0(&v);
    println!("{ld}");
}

Im Vergleich zu enums haben Trait-Objekte den Vorteil, dass nicht alle Gestalten feststehen müssen und z.B. auch Nutzer einer Bibliothek Funktionalität mit eigenen Gestalten verwenden können. Im Vergleich zu statischer Polymorphie mit generischen Typen bleibt die geringere Laufzeiteffizienz gegenüber kürzeren Kompilierzeiten, kleineren Kompilaten und der Möglichkeit vielgestaltiger Container.

Wir können auch Closures als Trait-Objekte auffassen mit äquivalenten Vor- und Nachteilen. Wir betrachten erneut ein Beispiel aus dem Grundlagenkapitel. Wir wollen einen element-weise definierten Operator auf alle Elemente eines Arrays anwenden.

#![allow(unused)]
fn main() {
/// Unary array operator
fn unary_arr_op(
    arr: [f64; 3], 
    f: fn(f64) -> f64
) -> [f64; 3] {
    let mut arr_res = [0.0; 3];
    arr_res[0] = f(arr[0]);
    arr_res[1] = f(arr[1]);
    arr_res[2] = f(arr[2]);
    arr_res
}
fn increase_by_10(arr: [f64; 3]) -> [f64; 3]{
    // kompiliert nicht, da die Variable `ten` außerhalb des Geltungsbereichs 
    // der anonymen Funktion incr ist
    // und incr somit keine einfache Funktion mehr ist
    let ten = 10.0;
    let incr = |x| x + ten;
    unary_arr_op(arr, incr)
}
}

Anstatt einer einfachen Funktion, die keine Variablen aus der Umgebung erfassen kann, können wir Closures mit statischer Polymorphie

#![allow(unused)]
fn main() {
/// Unary array operator
fn unary_arr_op<F>(
    arr: [f64; 3], 
    f: F
) -> [f64; 3]
where 
    F: Fn(f64) -> f64 {
    let mut arr_res = [0.0; 3];
    arr_res[0] = f(arr[0]);
    arr_res[1] = f(arr[1]);
    arr_res[2] = f(arr[2]);
    arr_res
}
fn increase_by_10(arr: [f64; 3]) -> [f64; 3]{
    let ten = 10.0;
    let incr = |x| x + ten;
    unary_arr_op(arr, incr)
}
}

und Closures mit dynamischer Polymophie

#![allow(unused)]
fn main() {
/// Unary array operator
fn unary_arr_op(
    arr: [f64; 3], 
    f: &dyn Fn(f64) -> f64
) -> [f64; 3] {
    let mut arr_res = [0.0; 3];
    arr_res[0] = f(arr[0]);
    arr_res[1] = f(arr[1]);
    arr_res[2] = f(arr[2]);
    arr_res
}
fn increase_by_10(arr: [f64; 3]) -> [f64; 3]{
    let ten = 10.0;
    let incr = |x| x + ten;
    unary_arr_op(arr, &incr)
}
}

verwenden.