Ein Tupel ist eine geordnete Sammlung von Werten potenziell unterschiedlicher Typen, mit zur Compile-Zeit fester Länge. Tupel sind in Rust ein vollwertiger Datentyp — sie werden by-value verschoben, können verschachtelt werden, lassen sich mit Pattern-Matching destrukturieren und tauchen häufig als kompakter Rückgabewert von Funktionen auf, die mehrere Werte liefern. Dieser Artikel zeigt die Syntax, den Feldzugriff per .0/.1, die Destrukturierung mit let-Patterns und ordnet Tupel gegenüber Tuple-Structs und Strukturen mit benannten Feldern ein.
Tupel-Syntax
Ein Tupel entsteht durch Klammerung von kommagetrennten Werten:
fn main() {
let punkt = (3.0, 4.0); // (f64, f64)
let person = ("Anna", 30, true); // (&str, i32, bool)
let einer = (42,); // 1-Tupel: Komma Pflicht
let leer = (); // Unit-Tupel (siehe unten)
println!("{} {} {}", person.0, person.1, person.2);
}Beachte das (42,): ohne das nachgestellte Komma würde (42) einfach 42 in Klammern sein, also i32. Das Komma macht es zu einem 1-Tupel vom Typ (i32,). In der Praxis selten gebraucht, aber gut zu wissen.
Tupel haben keine Felder-Namen — der Zugriff erfolgt über numerische Indices mit Punkt-Notation:
let dreier = (1, 2.0, "drei");
println!("{}", dreier.0); // 1
println!("{}", dreier.1); // 2.0
println!("{}", dreier.2); // "drei"Die Indices sind literal-Konstanten, nicht Variablen — let i = 0; dreier.i funktioniert nicht.
Destrukturierung
Eleganter als der .0-Zugriff ist die Destrukturierung per Pattern:
fn main() {
let punkt = (3.0, 4.0);
let (x, y) = punkt;
println!("x={x}, y={y}");
// Selektive Ignorierung mit _
let (_, vorname, _) = ("Müller", "Anna", 30);
// Pattern-Wildcard mit ..
let (erstes, .., letztes) = (1, 2, 3, 4, 5);
println!("{erstes}..{letztes}"); // 1..5
}Destrukturierung funktioniert in let, in Funktions-Parametern, in match-Armen — überall dort, wo ein Pattern legal ist.
fn distanz((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 {
((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}
fn main() {
let d = distanz((0.0, 0.0), (3.0, 4.0));
println!("{d}"); // 5.0
}Funktionen mit mehreren Rückgabewerten
Ein häufiges Pattern: eine Funktion gibt mehrere Werte als Tupel zurück:
fn min_max(zahlen: &[i32]) -> (i32, i32) {
let mut min = zahlen[0];
let mut max = zahlen[0];
for &n in zahlen {
if n < min { min = n; }
if n > max { max = n; }
}
(min, max)
}
fn main() {
let (lo, hi) = min_max(&[3, 1, 4, 1, 5, 9, 2, 6]);
println!("{lo}..{hi}"); // 1..9
}Sobald der Tupel-Inhalt drei Werte oder mehr hat — oder die Semantik der Felder unklar wird — lohnt sich der Wechsel zu einem Struct mit benannten Feldern (Kapitel Structs und Methoden).
Unit — das leere Tupel
Das Tupel ohne Elemente hat einen eigenen Namen: Unit (geschrieben ()):
let nichts: () = ();
println!("size_of::<()>(): {}", std::mem::size_of::<()>()); // 0() belegt 0 Bytes. Es ist der Typ aller Ausdrücke, die „keinen Wert" haben — etwa der Body einer Funktion ohne Rückgabewert:
fn nichts() { // Rückgabetyp implizit ()
println!("Hi");
}
fn nichts_explizit() -> () { // explizit, selten geschrieben
println!("Hi");
}
let x = nichts(); // x: ()Unit taucht überall auf, wo „kein Wert" das Ergebnis ist — Statements mit Semikolon, leere Blocks, Result<(), E> für Operationen ohne Erfolgswert. Eigener Artikel: Unit und Never.
Tupel im Speicher
Tupel sind Wert-Typen — kein indirekter Pointer, keine Heap-Allokation. Ein (i32, i64, u8) belegt im Speicher die Summe seiner Felder plus mögliches Padding für Alignment.
use std::mem::size_of;
println!("{}", size_of::<()>()); // 0
println!("{}", size_of::<(i32,)>()); // 4
println!("{}", size_of::<(i32, i32)>()); // 8
println!("{}", size_of::<(i32, i64)>()); // 16 — Padding!
println!("{}", size_of::<(u8, u8, u8)>()); // 3Das (i32, i64) belegt 16 Bytes (nicht 12), weil der i64 auf eine 8-Byte-Grenze ausgerichtet wird — Padding ist die Folge.
Die Reihenfolge der Felder im Speicher ist nicht garantiert zu der im Quellcode-Tupel; der Compiler darf reorganisieren, um Padding zu minimieren. Wer eine garantierte Layout-Reihenfolge braucht, nimmt ein #[repr(C)]-Struct.
Tupel vergleichen und kopieren
Tupel implementieren automatisch Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug — vorausgesetzt, alle ihre Elemente tun das auch.
let a = (1, 2);
let b = a; // Copy — a ist weiterhin verwendbar
println!("{:?} {:?}", a, b);
println!("{}", a == b); // true
println!("{}", (1, 2) < (1, 3)); // true — lexikographisch
let mit_string = (1, String::from("a"));
let kopiert = mit_string.clone(); // .clone() — kein Copy, da String nicht CopyEin Tupel mit String ist nicht Copy, weil String heap-alloziert und nicht trivial kopierbar ist. Clone funktioniert aber, solange alle Elemente Clone sind.
let punkt = (3.0, 4.0);
println!("{punkt:?}"); // (3.0, 4.0)
println!("{punkt:#?}"); // ausführlicher mit NewlinesDebug-Format gibt Tupel mit Klammern aus, wie sie auch im Quellcode aussehen.
Tupel vs. Tuple-Struct vs. Struct
Drei verwandte Konstrukte, wann welches?
| Konstrukt | Beispiel | Wann? |
|---|---|---|
| Tupel | (f64, f64) | Kurzlebig, lokaler Gebrauch, max. 3-4 Elemente |
| Tuple-Struct | struct Punkt(f64, f64); | Distinct Typ ohne Feld-Namen, oft für Newtype-Pattern |
| Struct mit Feldern | struct Punkt { x: f64, y: f64 } | Lesbarkeit ist wichtig, mehr als 2-3 Felder |
// Tupel — anonym
type Position = (f64, f64);
// Tuple-Struct — eigener Typ, Feldzugriff über .0/.1
struct Punkt(f64, f64);
// Newtype-Pattern (1-Element-Tuple-Struct)
struct Meter(f64);
// Klassischer Struct
struct Person {
vorname: String,
alter: u8,
}
impl Punkt {
fn x(&self) -> f64 { self.0 }
fn y(&self) -> f64 { self.1 }
}Faustregel:
- Zwei Werte, lokal genutzt: Tupel.
let (lo, hi) = min_max(...). - Domänen-spezifischer Typ ohne Feld-Bedeutung: Tuple-Struct (klassisch:
Meter,Wrapper<T>). - Mehrere Werte mit klarer Bedeutung: Struct mit Feldern.
Praxis: Tupel im Alltag
Statistik in einer Funktion
Min, Max und Mittelwert in einem Durchlauf — klassischer Mehrfach-Return-Use-Case:
fn min_max_avg(werte: &[f64]) -> Option<(f64, f64, f64)> {
if werte.is_empty() { return None; }
let mut min = werte[0];
let mut max = werte[0];
let mut summe = 0.0;
for &v in werte {
if v < min { min = v; }
if v > max { max = v; }
summe += v;
}
Some((min, max, summe / werte.len() as f64))
}
fn main() {
let messungen = [22.1, 19.8, 25.3, 21.0, 23.7];
if let Some((lo, hi, avg)) = min_max_avg(&messungen) {
println!("min={lo:.1} max={hi:.1} avg={avg:.2}");
}
}Drei Werte als Rückgabe sind in der Tupel-Form perfekt aufgehoben — bei vier oder fünf würde sich ein Struct lohnen.
HashMap-Iteration
Jeder Eintrag einer HashMap<K, V> taucht beim Iterieren als Tupel auf:
use std::collections::HashMap;
fn main() {
let mut zaehler: HashMap<&str, u32> = HashMap::new();
zaehler.insert("rot", 5);
zaehler.insert("blau", 12);
// Destrukturierendes Tupel im Loop
for (farbe, n) in &zaehler {
println!("{farbe}: {n}");
}
// Top-Eintrag finden
let top = zaehler.iter().max_by_key(|&(_, &n)| n);
if let Some((farbe, n)) = top {
println!("Spitzenreiter: {farbe} mit {n}");
}
}max_by_key schickt einen (&K, &V)-Tupel an die Closure. Pattern-Destrukturierung mit |&(_, &n)| fokussiert direkt auf den Wert.
enumerate für Index-Zugriff
Iterator::enumerate paart jedes Element mit seinem Index als Tupel:
fn main() {
let monate = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun"];
for (i, name) in monate.iter().enumerate() {
println!("{}. {name}", i + 1);
}
}Idiomatischer Ersatz für klassische for (int i = 0; i < len; i++)-Schleifen aus C — kein manuelles Inkrementieren, keine Index-Out-of-Bound-Risiken.
zip — zwei Iteratoren parallel
fn main() {
let namen = ["Anna", "Ben", "Clara"];
let alter = [28, 35, 41];
for (name, jahre) in namen.iter().zip(alter.iter()) {
println!("{name} ist {jahre}");
}
}zip zieht ein gepaartes Tupel pro Schritt. Häufig in Daten-Verarbeitungs-Pipelines, wo zwei zusammengehörige Sequenzen parallel laufen.
Interessantes
Tupel haben kein .iter().
Da Tupel-Elemente unterschiedlicher Typen sein können, gibt es keinen sinnvollen Iterator über sie. Wer „über die Elemente eines Tupels iterieren" will, hat fast immer einen Modellierungs-Fehler — ein Array oder Slice mit homogenem Typ wäre passender.
Maximal 12 Felder werden mit Trait-Implementierungen versorgt.
Clone, Eq, Debug und Co. werden in der Stdlib bis zu Tupel-Größe 12 automatisch implementiert. (i32, i32, ..., i32) mit 13 Feldern bekommt diese Traits nicht — der Compiler beschwert sich mit „the trait is not implemented for ...". In der Praxis nie ein Problem — bei mehr als 4-5 Feldern sollte ohnehin ein Struct verwendet werden.
Destrukturierung verschiebt — sei vorsichtig bei non-Copy-Typen.
let (a, b) = tupel; verschiebt die Werte aus dem Tupel in die Bindungen. Bei String, Vec und Co. ist das ein klassisches Move — das Original-Tupel ist danach nicht mehr verwendbar. Wer beide haben will: let (a, b) = tupel.clone(); oder Pattern mit &: let (a, b) = &tupel; (gibt Referenzen).
(x,) mit nachgestelltem Komma ist ein 1-Tupel — das normale (x) nicht.
Klassische Verwechslung bei Einsteigern. (5) ist 5 in Klammern — i32. (5,) ist ein 1-Tupel (i32,). Wichtig vor allem in Pattern-Matching, wo ein Pattern mit nur einem Element manchmal mit der Klammer-Form verwechselt wird.
Tupel sind Copy, wenn alle Felder es sind — aber nicht Default.
Default wird für Tupel nur bis Größe 12 implementiert, und nur wenn jedes Feld Default ist. <(i32, String)>::default() funktioniert ((0, "")), <(i32, NonDefaultType)>::default() nicht.
#[derive] auf Tupel-Aliasen geht nicht.
type Punkt = (f64, f64); #[derive(Debug)] impl ... ist kein gültiger Rust-Code — ein Type-Alias ist kein eigener Typ, sondern nur ein zweiter Name. Wer eigene Methoden oder Trait-Implementierungen will, nimmt ein Tuple-Struct oder einen klassischen Struct.
Tupel sind ein gutes „temporäres Bündel“ — aber kein Substitut für Structs.
Wenn (String, u8, bool) mehrmals in deinem Code auftaucht und immer die gleiche Bedeutung hat (Person mit Name, Alter, ist_aktiv), lohnt sich ein Struct. Tupel sind anonym — bei Code-Reviews muss man jedesmal mental dekodieren, was das .2 bedeutet.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – The Tuple Type
- Rust Reference – Tuple types
- std – Tuple Primitive
- Rust by Example – Tuples