Zum Inhalt springen

Tupel in Rust

Ein Tupel fasst mehrere Werte zu einem einzigen zusammen – und diese Werte dürfen unterschiedliche Typen haben. So bündelst du schnell zusammengehörige Dinge, ohne gleich einen eigenen Typ dafür zu definieren.

fn main() {
let person: (&str, i32, bool) = ("Alice", 30, true);
println!("{:?}", person);
}

Ausgabe:

("Alice", 30, true)
▶ Selbst ausprobieren

Das Tupel mischt hier einen &str, einen i32 und einen bool in einem Wert. Ein Array kann das nicht: Dort müssen alle Elemente denselben Typ haben.

Um ein ganzes Tupel auszugeben, nimmst du {:?} (Debug-Formatierung) statt {}. Ein einfaches {} funktioniert bei Tupeln nicht.

Mit .0, .1, .2 und so weiter greifst du auf die Elemente zu. Die Zählung beginnt bei null:

fn main() {
let person = ("Alice", 30, true);
println!("Name: {}", person.0);
println!("Alter: {}", person.1);
println!("Aktiv: {}", person.2);
}

Ausgabe:

Name: Alice
Alter: 30
Aktiv: true
▶ Selbst ausprobieren

Die Position muss eine feste Zahl sein, die direkt im Code steht. Eine Variable wie in person.i geht nicht: Jede Position kann einen anderen Typ haben, und Rust muss den Typ schon beim Kompilieren kennen.

Du kannst alle Werte in einer einzigen Zeile in eigene Variablen auspacken. Das nennt man Destrukturieren:

fn main() {
let person = ("Alice", 30, true);
let (name, age, is_active) = person;
println!("{name} ist {age}, aktiv: {is_active}");
}

Ausgabe:

Alice ist 30, aktiv: true
▶ Selbst ausprobieren

Das ist oft lesbarer als ständig .0, .1, .2 zu schreiben – vor allem, wenn ein Tupel mehr als zwei oder drei Felder hat.

Eine Funktion kann nur einen Rückgabetyp angeben. Willst du mehr als einen Wert zurückgeben, ist ein Tupel der natürliche Weg:

fn min_max(numbers: &[i32]) -> (i32, i32) {
let min = *numbers.iter().min().unwrap();
let max = *numbers.iter().max().unwrap();
(min, max)
}
fn main() {
let (smallest, largest) = min_max(&[4, 8, 1, 9, 3]);
println!("Minimum: {smallest}, Maximum: {largest}");
}

Ausgabe:

Minimum: 1, Maximum: 9
▶ Selbst ausprobieren

Mach dir um &[i32], .iter() und .unwrap() noch keine Gedanken. Die kommen auf den Seiten Slices, Iteratoren und Option dran. Wichtig ist hier der Rückgabetyp (i32, i32) – und wie main ihn sofort in zwei Variablen destrukturiert.

Ist ein Tupel mit mut deklariert, kannst du seine Felder einzeln ändern – über dieselben Positionen .0, .1. Die Werte dürfen sich ändern, die Form aber nicht: Ein Tupel behält immer die Anzahl und die Typen der Felder, mit denen es angefangen hat.

Destrukturieren funktioniert auch andersherum: Du kannst in bestehende Variablen zuweisen. So tauschst du zwei Werte elegant in einer Zeile, ganz ohne Hilfsvariable. Brauchst du nur einige der Felder, schreib _ für die, die dich nicht interessieren:

fn main() {
let mut point = (3, 4);
point.0 += 10; // ein Feld direkt ändern
println!("point ist {:?}", point);
let mut a = 1;
let mut b = 2;
(a, b) = (b, a); // die beiden Werte tauschen
println!("a = {a}, b = {b}");
let (x, _) = point; // x behalten, das zweite Feld ignorieren
println!("x = {x}");
}

Ausgabe:

point ist (13, 4)
a = 2, b = 1
x = 13
▶ Selbst ausprobieren

Der Tausch klappt, weil Rust zuerst die rechte Seite (b, a) baut. Erst danach packt es sie in a und b aus. Was du mit _ und Mustern sonst noch alles machen kannst, zeigt dir die Seite Pattern Matching.

Ein Tupel ist dir wahrscheinlich schon begegnet, ohne dass du es gemerkt hast. () heißt Unit-Typ. Diesen Wert gibt eine Funktion zurück, wenn sie nichts Sinnvolles zurückgibt – er ist zum Beispiel der unsichtbare Rückgabetyp von fn main().

Tupel sind ideal für schnelle Gruppierungen ohne Namen – besonders, wenn eine Funktion zwei oder drei Werte zurückgibt. Sobald eine Gruppe von Werten aber ein echtes Konzept mit benannten Feldern darstellt (etwa eine Person mit Name und Alter, auf die du im ganzen Code über die Namen zugreifst), ist ein Struct meist die bessere und lesbarere Wahl.

Schreib eine Funktion sum_and_product(a: i32, b: i32) -> (i32, i32), die sowohl die Summe als auch das Produkt zweier Zahlen zurückgibt. Ruf sie in main mit 5 und 6 auf und destrukturiere das Ergebnis. Tipp: Die letzte Zeile der Funktion ist das Tupel selbst, (a + b, a * b), ohne Semikolon.

Lösung anzeigen
fn sum_and_product(a: i32, b: i32) -> (i32, i32) {
(a + b, a * b)
}
fn main() {
let (sum, product) = sum_and_product(5, 6);
println!("Summe = {sum}, Produkt = {product}");
}

Ausgabe:

Summe = 11, Produkt = 30
▶ Selbst ausprobieren

Ein Rechteck lässt sich als Tupel aus zwei Punkten speichern, wobei jeder Punkt selbst ein Tupel ist: let rect = ((1, 2), (5, 5));. Berechne Breite, Höhe und Fläche. Tipp: Tupel lassen sich verschachteln, und rect.1.0 bedeutet „das erste Feld des zweiten Punkts“.

Lösung anzeigen
fn main() {
let rect = ((1, 2), (5, 5));
let width = rect.1.0 - rect.0.0;
let height = rect.1.1 - rect.0.1;
println!("Breite {width}, Höhe {height}, Fläche {}", width * height);
}

Ausgabe:

Breite 4, Höhe 3, Fläche 12
▶ Selbst ausprobieren

Du kannst auch beide Punkte auf einmal destrukturieren: let ((x1, y1), (x2, y2)) = rect;. Das ist mehr Tipparbeit, aber x2 - x1 liest sich leichter als rect.1.0 - rect.0.0. Wenn sich die Punkte so stapeln, ist das ein Zeichen, dass ein Struct mit benannten Feldern klarer wäre.

  • Ein Tupel bündelt Werte, die unterschiedliche Typen haben dürfen: (name, age, is_active).
  • Auf die Felder greifst du mit .0, .1, .2 zu, oder du packst sie per Destrukturieren alle auf einmal aus.
  • Mit Tupeln gibst du ganz natürlich mehrere Werte aus einer Funktion zurück.
  • Die Felder eines mut-Tupels können sich ändern, seine Größe und Typen aber nicht. (a, b) = (b, a); tauscht zwei Werte.
  • Für benannte, wiederverwendbare Gruppierungen nimmst du besser ein Struct.

Kurzer Check

1. Was kann ein Tupel enthalten, ein Array aber nicht?

2. Wie greifst du auf das zweite Element eines Tupels namens person zu?

3. Welche Werte haben a und b nach let mut a = 1; let mut b = 2; (a, b) = (b, a);?

Punkte: 0 / 3