Generische Typen (Generics) ermöglichen es, Code unabhängig von einem bestimmten Typ zu schreiben. Sie werden mit der Syntax von spitzen Klammern realisiert:
fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
Hier ist T der generische Typ, der durch das Trait PartialOrd eingeschränkt ist.
Generische Parameter werden durch <T> deklariert, können aber durch Trait Bounds mit einem Doppelpunkt eingeschränkt werden, zum Beispiel <T: Display>. Dies ist ein Weg, dem Compiler mitzuteilen, dass nur die Typen verwendet werden können, für die das benötigte Trait implementiert ist.
In Rust gibt es zwei Formen der Dispatch für Generics:
dyn Trait verwendet wird, erfolgt der Aufruf über eine virtuelle Tabelle (vtable).Einfluss auf den Maschinencode: Die Verwendung von Generics mit Trait Bounds (ohne dyn Trait) führt zur Monomorphisierung: einer Erhöhung der Binärdatei, aber maximaler Geschwindigkeit. Die Verwendung von dyn Trait spart Binärdatei, führt jedoch zu einem Performanceeinbruch.
Frage: Gibt es eine Funktion
fn do_something<T: Debug>(value: &T)
Wird der Compiler separate Funktionen do_something im Binärcode für jeden Typ erstellen, mit dem sie verwendet wird, oder wird er eine universelle Implementierung verwenden?
Typische falsche Antwort: Es wird eine Funktion für alle Typen durch den Trait Bound verwendet.
Richtige Antwort: Der Compiler erstellt separate Kopien dieser Funktion für jeden Typ (Monomorphisierung), da der Trait Bound die generische Funktion nicht "universell" durch die vtable macht. Universelle Implementierung tritt nur bei dyn Trait (dynamischer Dispatch) auf.
Beispiel:
fn print_val<T: std::fmt::Debug>(val: T) { println!("{:?}", val); } // Für jeden Aufruf mit einem anderen Typ wird eine eigene Version der Funktion erstellt
Geschichte
In einem Projekt mit großen generischen Objekten wurde festgestellt, dass die Binärdatei erheblich größer wurde als erwartet. Später stellte sich heraus: Der Grund war die umfangreiche Verwendung von generischen Funktionen ohne Einschränkungen. Aufrufe mit Dutzenden von Typen führten zu einem exponentiellen Wachstum der Größe der ausführbaren Datei (Code Bloat), was erst bei der Release-Build auf CI festgestellt wurde.
Geschichte
Einer der Entwickler akzeptierte einen generischen Parameter mit Trait Bound, in der Annahme, dass dieser Code mit "dynamischem" Dispatch funktioniert. Dies führte zu einem übermäßigen Speicherverbrauch auf dem Server und einer Verringerung der Leistung aufgrund des ständigen Wachstums des Codes und dessen Caching durch die CPU.
Geschichte
In der Bibliothek wurde versucht, ein generisches Trait mit Self-Typ (z.B. Trait Clone) als dyn Trait zu verwenden, was in Rust nicht unterstützt wird und zu einem Kompilierungsfehler führte. Das Interface musste ausdrücklich neu geschrieben werden, andernfalls würde das generische API im dynamischen Modus nicht funktionieren, und das Interface müsste auf Compile-Time-Ebene geändert werden.