Zum Inhalt springen

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.

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.

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:

47
2.8
▶ Selbst ausprobieren

<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).

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 10
hallo Welt
▶ Selbst ausprobieren

Eine 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.

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ü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 links
8
▶ Selbst ausprobieren

Hier gibt es zwei Arten von impl-Blöcken:

  • impl<T> Pair<T> bedeutet: „Diese Methoden gibt es für ein Pair mit beliebigem Typ.“ Lies das erste <T> als „für jeden Typ T“. Ohne es würde Rust denken, T sei 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ür Pair<f64>. Deshalb darf average mit + und / 2.0 rechnen. Ruf testweise words.average() auf, dann sagt dir der Compiler, dass es diese Methode für Pair<&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.

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:

3
r
▶ Selbst ausprobieren

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:

9
Birne
▶ Selbst ausprobieren

Strings werden alphabetisch verglichen: "Birne" beginnt mit B und kommt damit nach "Apfel", gilt also als „größer“.

  • 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, was T sein darf.
  • impl<T> Pair<T> fügt Methoden für jedes T hinzu; 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

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