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:

Rust Tupel-Basics
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:

Rust Feldzugriff
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:

Rust Destrukturierung
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.

Rust Funktions-Parameter
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:

Rust Multi-Return
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 ()):

Rust Unit
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:

Rust Unit-Rückgaben
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.

Rust Speicher-Layout
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)>());          // 3

Das (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.

Rust Tupel-Operationen
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 Copy

Ein Tupel mit String ist nicht Copy, weil String heap-alloziert und nicht trivial kopierbar ist. Clone funktioniert aber, solange alle Elemente Clone sind.

Rust Debug-Output
let punkt = (3.0, 4.0);
println!("{punkt:?}");         // (3.0, 4.0)
println!("{punkt:#?}");        // ausführlicher mit Newlines

Debug-Format gibt Tupel mit Klammern aus, wie sie auch im Quellcode aussehen.

Tupel vs. Tuple-Struct vs. Struct

Drei verwandte Konstrukte, wann welches?

KonstruktBeispielWann?
Tupel(f64, f64)Kurzlebig, lokaler Gebrauch, max. 3-4 Elemente
Tuple-Structstruct Punkt(f64, f64);Distinct Typ ohne Feld-Namen, oft für Newtype-Pattern
Struct mit Feldernstruct Punkt { x: f64, y: f64 }Lesbarkeit ist wichtig, mehr als 2-3 Felder
Rust Vergleich
// 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:

Rust Stats
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:

Rust HashMap-Iteration
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:

Rust Index in for-Loop
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

Rust zip
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

/ Weiter

Zurück zu Primitive Datentypen

Zur Übersicht