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 enum
s 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.