Generische Datentypen, Traits und Lebenszeiten
Verständnis
Generics, Variante 1: Datentypen
// 泛型声明 - struct struct Point<T> { x: T, y: T, } let point = Point { x: 5, y: 4 }; // 必须是同一类型 struct PointDiffType<T, U> { x: T, y: U, } let point_diff_type = PointDiffType{ x: 5, y: 4.0 }; // 不同类型 // 泛型声明 - impl impl<T> Point<T> { fn x(&self) -> &T { &self.x } } // 泛型声明 - enum enum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E), } // 一种 mix-up 实现 struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { // 由于泛型的高灵活性,这里做了巧妙的 field 交叉混合,组合出第三个新泛型实例 // 这种语法可用于 middleware fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }Die Verwendung von Generics beeinflusst die Laufzeitleistung nicht
Denn der Monomorphisierungsprozess wandelt Generics bereits beim Kompilieren in konkrete Typen um.
// 比如使用了 Some 泛型代码 let integer = Some(5); let float = Some(5.0); // 编译期会 Monomorphization 为以下代码 enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); }Generics, Variante 2: Traits und Trait Bounds
Ein Trait beschreibt eine Eigenschaft. Grundsätzlich kann er Structs, Funktionsparameter und Rückgabewerte mit einer Eigenschaft versehen.
Bei einem Struct kann der Trait festlegen, dass es die darin deklarierten Methoden besitzt, ähnlich wie ein Java-
interface.impl structA for traitABei einem generischen Typ kann der Trait festlegen, dass nur konkrete Typen mit diesen Eigenschaften (Trait Bounds) die entsprechende Methodensammlung erhalten.
impl<T: traitA + traitB + ...> structA<T> {...}Bei Funktionsparametern kann der Trait einschränken, dass nur Parameter mit den entsprechenden Eigenschaften verwendet werden dürfen (Polymorphie).
pub fn notify(item: &impl Summary) {...}Bei einem Rückgabewert kann der Trait garantieren, dass der Rückgabewert diese Eigenschaft besitzt.
// Defining a Trait pub trait Summary { fn summarize(&self) -> String; } // Implementing a Trait on a Type pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } // Default Implementations pub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } } // Default Implementations 可以调用其它 impl pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } } // Traits as Parameters pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); } // 可以使用 generic type 来指定 trait bounds pub fn notify<T: Summary>(item1: &T, item2: &T) {} // 多个 trait 的情况 1 - 使用 + 操作符 pub fn notify(item: &(impl Summary + Display)) { pub fn notify<T: Summary + Display>(item: &T) { // 多个 trait 的情况 2 - 使用 where fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug // Returning Types that Implement Traits fn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } } // 可以通过 trait bounds 特性,以实现:有对应 trait 的 generic type,才能有对应的 method use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } }Generics, Variante 3: Lebenszeiten
&i32 // a reference &'a i32 // a reference with an explicit lifetime &'a mut i32 // a mutable reference with an explicit lifetime // 一般用在 function 定义时 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // 可用于 struct struct ImportantExcerpt<'a> { part: &'a str, } // Lifetime Elision - 活用 3 条规则 fn first_word(s: &str) -> &str {...} // static 生命期,可以在整个程序运行时保持可用 let s: &'static str = "I have a static lifetime.";Der Compiler verwendet drei Regeln, um Lebenszeiten von Referenzen ohne explizite Annotationen zu bestimmen.
Die erste Regel besagt, dass jeder Referenzparameter seinen eigenen Lebenszeitparameter erhält.
Erste Regel: Jeder Referenzparameter erhält einen eigenen Lebenszeitparameter.
Die zweite Regel besagt: Gibt es genau einen Eingabe-Lebenszeitparameter, wird diese Lebenszeit allen Ausgabe-Lebenszeitparametern zugewiesen.
Zweite Regel: Bei genau einem Eingabe-Lebenszeitparameter wird dieser allen Ausgabe-Lebenszeitparametern zugewiesen.
Die dritte Regel besagt: Gibt es mehrere Eingabe-Lebenszeitparameter und ist einer davon wegen einer Methode
&selfoder&mut self, wird die Lebenszeit vonselfallen Ausgabe-Lebenszeitparametern zugewiesen.Dritte Regel: Bei mehreren Eingabe-Lebenszeitparametern wird die Lebenszeit von
selfallen Ausgaben zugewiesen, wenn einer&selfoder&mut selfist.Kombination der drei Varianten
use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: Display, { println!("Announcement! {}", ann); if x.len() > y.len() { x } else { y } }
Quelle
…
Leistung von Code mit Generics
Vielleicht fragst du dich, ob die Verwendung generischer Typparameter Laufzeitkosten verursacht. Die gute Nachricht: Rust implementiert Generics so, dass Code mit generischen Typen nicht langsamer läuft als Code mit konkreten Typen.
Rust erreicht dies durch Monomorphisierung des generischen Codes zur Kompilierzeit. Monomorphisierung bedeutet, generischen Code in spezifischen Code umzuwandeln, indem beim Kompilieren die verwendeten konkreten Typen eingesetzt werden.
…
Der Compiler verwendet drei Regeln, um Lebenszeiten von Referenzen ohne explizite Annotationen zu bestimmen. Die erste Regel betrifft Eingabe-Lebenszeiten, die zweite und dritte Ausgabe-Lebenszeiten. Wenn nach allen drei Regeln noch Referenzen mit unbestimmter Lebenszeit übrig bleiben, bricht der Compiler mit einem Fehler ab. Die Regeln gelten sowohl für fn-Definitionen als auch für impl-Blöcke.
Die erste Regel: Jeder Referenzparameter erhält einen eigenen Lebenszeitparameter. Eine Funktion mit einem Parameter erhält also einen Parameter: fn foo<'a>(x: &'a i32); eine Funktion mit zwei Parametern erhält zwei getrennte Parameter: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); und so weiter.
Die zweite Regel: Gibt es genau einen Eingabe-Lebenszeitparameter, wird diese Lebenszeit allen Ausgabe-Lebenszeitparametern zugewiesen: fn foo<'a>(x: &'a i32) -> &'a i32.
Die dritte Regel: Gibt es mehrere Eingabe-Lebenszeitparameter und ist einer davon &self oder &mut self, weil es sich um eine Methode handelt, wird die Lebenszeit von self allen Ausgabe-Lebenszeitparametern zugewiesen. Dadurch sind Methoden angenehmer zu lesen und zu schreiben, weil weniger Symbole nötig sind.