Generics in Rust
Generics (generische Typen) hast du schon benutzt, vielleicht ohne es zu merken: Vec<i32> und Option<T> verwenden sie beide. Auf dieser Seite schreibst du eigene generische Funktionen und Structs.
Welches Problem Generics lösen
Abschnitt betitelt „Welches Problem Generics lösen“Angenommen, du möchtest eine Funktion, die die größte Zahl aus einer Liste zurückgibt – sowohl für i32 als auch für f64. Ohne Generics müsstest du sie zweimal schreiben:
fn largest_i32(numbers: &[i32]) -> i32 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn largest_f64(numbers: &[f64]) -> f64 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}Dieselbe Logik, für jeden Typ kopiert. Mit Generics schreibst du sie nur einmal.
Eine generische Funktion
Abschnitt betitelt „Eine generische Funktion“fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn main() { let integers = [10, 25, 3, 47, 8]; let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers)); println!("{}", largest(&floats));}Ausgabe:
472.8<T: PartialOrd + Copy> liest sich so: „für jeden Typ T, solange er sich vergleichen (PartialOrd) und kopieren (Copy) lässt“. T ist ein Platzhalter. Rust erzeugt eine Version der Funktion für jeden konkreten Typ, mit dem du sie tatsächlich aufrufst (i32, f64 und so weiter).
Ein generisches Struct
Abschnitt betitelt „Ein generisches Struct“Auch Structs können generisch sein:
struct Pair<T> { first: T, second: T,}
fn main() { let numbers = Pair { first: 5, second: 10 }; let words = Pair { first: "hallo", second: "Welt" };
println!("{} {}", numbers.first, numbers.second); println!("{} {}", words.first, words.second);}Ausgabe:
5 10hallo WeltEine einzige Definition von Pair<T> funktioniert für Zahlen, Text oder jeden anderen Typ. numbers ist ein Pair<i32> und words ein Pair<&str> – geschrieben hast du das Struct aber nur einmal.
Kostet das etwas zur Laufzeit?
Abschnitt betitelt „Kostet das etwas zur Laufzeit?“Nein. Rust erzeugt für jeden konkreten Typ, den du tatsächlich benutzt, eine eigene, spezialisierte Version der Funktion oder des Structs (das nennt man Monomorphisierung). Wenn dein Programm läuft, ist kein generischer Code mehr übrig – nur ganz normale Funktionen mit festen Typen. Die sind genauso schnell, als hättest du jede einzelne von Hand geschrieben.
Methoden für ein generisches Struct
Abschnitt betitelt „Methoden für ein generisches Struct“Methoden fügst du einem generischen Struct mit einem impl-Block hinzu, genau wie auf der Seite Structs. Neu ist nur, dass auch der impl-Block ein eigenes <T> braucht:
struct Pair<T> { first: T, second: T,}
impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Pair { first, second } }
fn swap(self) -> Pair<T> { Pair { first: self.second, second: self.first } }}
impl Pair<f64> { fn average(&self) -> f64 { (self.first + self.second) / 2.0 }}
fn main() { let words = Pair::new("links", "rechts").swap(); println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0); println!("{}", scores.average());}Ausgabe:
rechts links8Hier gibt es zwei Arten von impl-Blöcken:
impl<T> Pair<T>bedeutet: „Diese Methoden gibt es für einPairmit beliebigem Typ.“ Lies das erste<T>als „für jeden TypT“. Ohne es würde Rust denken,Tsei der Name eines echten Typs, und sich beschweren, dass es ihn nicht findet.impl Pair<f64>hat kein<T>. Seine Methoden gibt es nur fürPair<f64>. Deshalb darfaveragemit+und/ 2.0rechnen. Ruf testweisewords.average()auf, dann sagt dir der Compiler, dass es diese Methode fürPair<&str>nicht gibt.
Ein Struct kann auch mehr als einen Typparameter haben, etwa struct Pair<T, U> { first: T, second: U }, wenn die beiden Felder verschiedene Typen haben dürfen. Und wenn die Trait Bounds lang werden, kannst du sie in eine where-Klausel verschieben, damit die Signatur lesbar bleibt. Mehr dazu auf der Seite Fortgeschrittene Traits.
Übungen
Abschnitt betitelt „Übungen“1. Ein generisches smallest
Abschnitt betitelt „1. Ein generisches smallest“Schreib fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T, die das kleinste Element zurückgibt. Teste sie mit [8, 3, 5] und mit den Zeichen ['r', 'u', 's', 't']. Tipp: Das ist die Funktion largest von dieser Seite, nur mit einem umgedrehten Vergleich.
Lösung anzeigen
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T { let mut smallest = items[0]; for &item in items { if item < smallest { smallest = item; } } smallest}
fn main() { println!("{}", smallest(&[8, 3, 5])); println!("{}", smallest(&['r', 'u', 's', 't']));}Ausgabe:
3r2. Die größere Hälfte eines Pair
Abschnitt betitelt „2. Die größere Hälfte eines Pair“Füge Pair<T> eine Methode larger(&self) -> &T hinzu, die eine Referenz auf das größere der beiden Felder zurückgibt. Teste sie mit einem Pair aus Zahlen und einem Pair aus Wörtern. Tipp: Zum Vergleichen brauchst du PartialOrd. Schreib die Schranke deshalb direkt an den impl-Block: impl<T: PartialOrd> Pair<T>.
Lösung anzeigen
struct Pair<T> { first: T, second: T,}
impl<T: PartialOrd> Pair<T> { fn larger(&self) -> &T { if self.first > self.second { &self.first } else { &self.second } }}
fn main() { let numbers = Pair { first: 3, second: 9 }; let words = Pair { first: "Birne", second: "Apfel" };
println!("{}", numbers.larger()); println!("{}", words.larger());}Ausgabe:
9BirneStrings werden alphabetisch verglichen: "Birne" beginnt mit B und kommt damit nach "Apfel", gilt also als „größer“.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Mit Generics schreibst du eine Funktion oder ein Struct, das mit mehreren Typen funktioniert, statt Code zu kopieren.
<T>ist ein Platzhaltertyp; Trait Bounds (T: PartialOrd) schränken ein, wasTsein darf.impl<T> Pair<T>fügt Methoden für jedesThinzu;impl Pair<f64>fügt Methoden nur für einen konkreten Typ hinzu.- Generischer Code wird pro Typ in spezialisierte Versionen übersetzt – das kostet zur Laufzeit keine Leistung.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Welches Problem lösen Generics?
2. Was bewirkt PartialOrd + Copy in fn largest<T: PartialOrd + Copy>(...)?
3. Kosten Generics zur Laufzeit etwas im Vergleich zu handgeschriebenem Code?
4. Was ermöglicht dir eine einzige Definition von struct Pair<T>?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Generische Datentypen – Das Rust-Buch: Generics in Funktionen, Structs, Enums und Methoden, einschließlich des Beispiels
largest. - Generics – Rust by Example (auf Englisch): kurze, ausführbare Beispiele für generische Funktionen, Structs und ihre Syntax.
- Implementation – Rust by Example (auf Englisch): mehr zu
impl<T>-Blöcken und dazu, wie du Methoden nur für einen konkreten Typ implementierst. - Bounds – Rust by Example (auf Englisch): wie Trait Bounds einschränken, was ein generischer Typ sein darf, und warum du sie brauchst.
- Generic parameters – The Rust Reference (auf Englisch): die genauen Regeln, wenn du alle Details wissen willst.