Tuple-Structs sind die zweite Form von Structs: anstelle benannter Felder haben sie positionale, wie bei einem Tupel. Sie sind eine schmale, aber wichtige Variante mit zwei Hauptanwendungen — als Newtype-Wrapper (Struct mit genau einem Feld, um Typsicherheit zu erzeugen) und als kleine geometrische Typen wie Koordinaten oder RGB-Farben. Dieser Artikel zeigt, wann ein Tuple-Struct dem klassischen Field-Struct vorzuziehen ist, wie er sich vom anonymen Tupel unterscheidet, und welche Patterns aus ihm einen unverzichtbaren Werkzeug der Sprache machen.

Deklaration

Rust Tuple-Struct
struct Punkt(f64, f64);
struct RGB(u8, u8, u8);
struct Tag(u32);

Syntax:

  • struct als Schlüsselwort.
  • Name in UpperCamelCase.
  • Felder in runden Klammern, nur Typen — keine Namen.

Jeder Tuple-Struct ist ein eigener Typ. Punkt(1.0, 2.0) ist nicht das gleiche wie das anonyme Tupel (1.0, 2.0) — auch wenn sie strukturell identisch aussehen.

Konstruktion und Zugriff

Rust Verwendung
struct Punkt(f64, f64);

fn main() {
    let p = Punkt(3.0, 4.0);
    println!("x={}, y={}", p.0, p.1);

    // Mutation (Bindung muss mut sein):
    let mut q = Punkt(0.0, 0.0);
    q.0 = 5.0;
}

Zugriff via Index-Notation: p.0, p.1 — die Indices sind literale Konstanten (nicht Variablen). p.i mit i als Variable funktioniert nicht.

Pattern-Destrukturierung

Rust Patterns
# struct Punkt(f64, f64);
let p = Punkt(3.0, 4.0);
let Punkt(x, y) = p;
println!("{x} {y}");

// Selektiv mit _:
let Punkt(x, _) = Punkt(7.0, 8.0);

// In match-Armen:
# let p2 = Punkt(0.0, 0.0);
match p2 {
    Punkt(0.0, 0.0) => println!("Ursprung"),
    Punkt(x, 0.0) => println!("Auf x-Achse bei {x}"),
    Punkt(0.0, y) => println!("Auf y-Achse bei {y}"),
    Punkt(_, _) => println!("woanders"),
}

Pattern-Matching auf Tuple-Structs ist sehr ausdrucksstark — wie bei anonymen Tupeln, aber mit dem zusätzlichen Typ-Match.

Tuple-Struct vs. anonymes Tupel

Strukturell identisch, semantisch unterschiedlich:

Rust Anonym vs. genannt
// Anonymes Tupel:
let anonym: (f64, f64) = (3.0, 4.0);

// Tuple-Struct:
struct Punkt(f64, f64);
let punkt: Punkt = Punkt(3.0, 4.0);

// Beide haben identisches Memory-Layout, aber:
// - Punkt ist ein eigener Typ
// - Anonyme Tupel sind strukturell typisiert ((f64, f64))

fn distanz_zu_null(p: Punkt) -> f64 {
    (p.0 * p.0 + p.1 * p.1).sqrt()
}

// distanz_zu_null(anonym);     // Fehler — (f64, f64) ≠ Punkt

Tuple-Structs sind nominal: zwei verschiedene Tuple-Structs mit identischen Feldern sind verschiedene Typen. Das ist genau der Vorteil — du kannst Typsicherheit ohne Field-Namen erzeugen.

AspektAnonymes TupelTuple-Struct
Eigener Typ?Nein, strukturellJa, nominal
Methoden möglich?Nein direkt (nur Trait-Impls)Ja (via impl)
derive?Nicht direktJa
Wann nutzenKurzlebige, lokale BündelDomain-Typen, Newtype

Der Newtype-Spezialfall

Ein Tuple-Struct mit genau einem Feld heißt Newtype:

Rust Newtype
struct Meter(f64);
struct Sekunden(u64);
struct UserId(u64);

Auf den ersten Blick: ein Wrapper, der scheinbar nichts hinzufügt. In Wirklichkeit: ein eigener Typ, der Typsicherheit garantiert. Die folgenden Beispiele sind alle Compile-Fehler:

Rust Typsicherheit
# struct Meter(f64);
# struct Sekunden(u64);
# struct UserId(u64);
let strecke = Meter(100.0);
let dauer = Sekunden(60);
let user = UserId(42);

// strecke + dauer;          // Fehler — keine Add-Impl
// strecke == dauer;         // Fehler — verschiedene Typen
// user + 1;                  // Fehler — UserId + i32

Newtype-Pattern ist eines der wichtigsten in idiomatischem Rust. Ein eigenes Kapitel dazu folgt in diesem Abschnitt.

Methoden auf Tuple-Structs

Rust impl
struct Punkt(f64, f64);

impl Punkt {
    fn neu(x: f64, y: f64) -> Self {
        Punkt(x, y)
    }
    fn x(&self) -> f64 { self.0 }
    fn y(&self) -> f64 { self.1 }
    fn distanz_zu_null(&self) -> f64 {
        (self.0 * self.0 + self.1 * self.1).sqrt()
    }
}

fn main() {
    let p = Punkt::neu(3.0, 4.0);
    assert_eq!(p.distanz_zu_null(), 5.0);
}

Wie bei Field-Structs: impl Tuple-Struct { ... } fügt Methoden hinzu. Die Methoden können named Accessoren bereitstellen (fn x(&self) -> f64), wenn die Index-Notation .0/.1 unleserlich wird.

Mehrere Felder

Tuple-Structs mit mehreren Feldern sind selten — meist ist ein Field-Struct lesbarer:

Rust Vergleich
// Geht, aber bei mehr als 2-3 Feldern verwirrend:
struct PersonTuple(String, u32, bool);

// Bei semantisch unterschiedlichen Feldern besser:
struct PersonFields {
    name: String,
    alter: u32,
    aktiv: bool,
}

fn main() {
    let p = PersonTuple("Anna".into(), 28, true);
    // p.0 — was war das nochmal? Der Name?
    println!("{}", p.0);

    let q = PersonFields { name: "Bert".into(), alter: 30, aktiv: false };
    // q.name — selbsterklärend
    println!("{}", q.name);
}

Faustregel: Tuple-Structs mit 1 Feld (Newtype) — sehr häufig. Mit 2-3 Feldern wie Koordinaten oder RGB — gelegentlich. Mit 4+ Feldern — fast immer schlechter als ein Field-Struct.

Sichtbarkeit der Felder

Wie bei Field-Structs: Felder können einzeln pub markiert werden — hier auf positionaler Basis:

Rust Field-Visibility
pub struct Punkt(pub f64, pub f64);          // beide öffentlich
pub struct Geheim(pub i32, String);           // erstes pub, zweites privat

Bei rein privaten Feldern brauchst du einen Konstruktor — Code außerhalb des Moduls kann den Struct sonst nicht erstellen.

Praxis: Tuple-Structs im echten Code

RGB-Farbe

Rust RGB
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rgb(pub u8, pub u8, pub u8);

impl Rgb {
    pub const SCHWARZ: Rgb = Rgb(0, 0, 0);
    pub const WEISS: Rgb = Rgb(255, 255, 255);

    pub fn r(&self) -> u8 { self.0 }
    pub fn g(&self) -> u8 { self.1 }
    pub fn b(&self) -> u8 { self.2 }

    pub fn hell(&self) -> bool {
        (self.0 as u32 + self.1 as u32 + self.2 as u32) > 384
    }

    pub fn als_hex(&self) -> String {
        format!("#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
    }
}

fn main() {
    let rot = Rgb(255, 0, 0);
    assert_eq!(rot.als_hex(), "#ff0000");
    assert!(!rot.hell());
}

Drei u8-Werte als RGB. Tuple-Struct passt — die Reihenfolge (R, G, B) ist Konvention und intuitiv.

IPv4-Adresse

Rust IPv4
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Ipv4(pub u8, pub u8, pub u8, pub u8);

impl Ipv4 {
    pub const LOCALHOST: Ipv4 = Ipv4(127, 0, 0, 1);

    pub fn ist_privat(&self) -> bool {
        matches!(self,
            Ipv4(10, _, _, _)
            | Ipv4(172, 16..=31, _, _)
            | Ipv4(192, 168, _, _)
        )
    }

    pub fn als_string(&self) -> String {
        format!("{}.{}.{}.{}", self.0, self.1, self.2, self.3)
    }
}

fn main() {
    let lokal = Ipv4(192, 168, 1, 1);
    assert!(lokal.ist_privat());
    assert_eq!(lokal.als_string(), "192.168.1.1");
}

Ipv4(a, b, c, d) ist semantisch klarer als [u8; 4], ohne derive(...) zu verlieren.

Newtype für API-Token

Rust Token-Wrapper
pub struct ApiToken(String);

impl ApiToken {
    pub fn neu(roh: String) -> Result<Self, &'static str> {
        if roh.len() < 32 { return Err("zu kurz"); }
        Ok(ApiToken(roh))
    }

    pub fn als_str(&self) -> &str { &self.0 }
}

fn main() {
    let t = ApiToken::neu(String::from("a".repeat(40))).unwrap();
    println!("{}", t.als_str());
    // t.0 — direkter Zugriff nicht möglich (privat)
}

Tuple-Struct mit privatem Feld plus Konstruktor mit Validierung — kein anderer Code-Pfad kann einen unvaliden ApiToken erzeugen.

Domain-Typ für UserId

Rust UserId
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct UserId(pub u64);

pub fn finde_user(id: UserId) -> Option<String> {
    // Niemand kann versehentlich eine OrderId oder ProductId übergeben
    None
}

fn main() {
    let id = UserId(42);
    finde_user(id);
    // finde_user(42);        // Fehler — kein u64
}

Klassische Newtype-Anwendung für stark-typisierte IDs. UserId(42), OrderId(42), ProductId(42) sind drei verschiedene Typen — Verwechslungs-sicher.

Geld-Wert in Cents

Rust Geld
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct EurCent(pub i64);

impl EurCent {
    pub const NULL: EurCent = EurCent(0);

    pub fn plus(self, other: EurCent) -> EurCent {
        EurCent(self.0 + other.0)
    }

    pub fn als_euro_string(&self) -> String {
        let euro = self.0 / 100;
        let cent = (self.0 % 100).abs();
        format!("{},{:02} €", euro, cent)
    }
}

fn main() {
    let preis = EurCent(1990);
    let mwst = EurCent(380);
    let brutto = preis.plus(mwst);
    assert_eq!(brutto.als_euro_string(), "23,70 €");
}

Geld als i64-Cent — keine Float-Rundungsfehler. Newtype-Pattern macht's typsicher.

Sekunden vs. Millisekunden

Rust Time-Units
#[derive(Clone, Copy, Debug)]
pub struct Sekunden(pub u64);

#[derive(Clone, Copy, Debug)]
pub struct Millisekunden(pub u64);

impl From<Sekunden> for Millisekunden {
    fn from(s: Sekunden) -> Self {
        Millisekunden(s.0 * 1000)
    }
}

fn warte(dauer: Millisekunden) {
    std::thread::sleep(std::time::Duration::from_millis(dauer.0));
}

fn main() {
    warte(Millisekunden(500));
    warte(Sekunden(2).into());      // Implizite Konvertierung über From
}

From-Impl macht Konvertierung explizit aber bequem. Verwechslung von Sekunden mit Millisekunden ist Compile-Fehler.

Punkt im 2D-Raum

Rust 2D-Punkt
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Punkt2D(pub f64, pub f64);

impl Punkt2D {
    pub fn neu(x: f64, y: f64) -> Self { Punkt2D(x, y) }
    pub fn distanz_zu(self, other: Punkt2D) -> f64 {
        let dx = self.0 - other.0;
        let dy = self.1 - other.1;
        (dx * dx + dy * dy).sqrt()
    }
}

fn main() {
    let a = Punkt2D::neu(0.0, 0.0);
    let b = Punkt2D::neu(3.0, 4.0);
    assert_eq!(a.distanz_zu(b), 5.0);
}

Tuple-Struct für mathematische Punkte. (x, y) ist Konvention — Field-Struct mit { x, y } wäre eine Alternative.

Wrapper über Stdlib-Typ

Rust HashSet-Wrapper
use std::collections::HashSet;

// Newtype über HashSet, der eigene Trait-Implementierungen erlaubt:
pub struct EindeutigeIds(HashSet<u64>);

impl EindeutigeIds {
    pub fn neu() -> Self { EindeutigeIds(HashSet::new()) }

    pub fn hinzufuegen(&mut self, id: u64) -> bool {
        self.0.insert(id)
    }

    pub fn anzahl(&self) -> usize { self.0.len() }
}

fn main() {
    let mut set = EindeutigeIds::neu();
    assert!(set.hinzufuegen(42));
    assert!(!set.hinzufuegen(42));      // schon drin
    assert_eq!(set.anzahl(), 1);
}

Newtype über HashSet<u64> — verbirgt die interne Repräsentation und erlaubt eigene Methoden mit Domain-Semantik.

Besonderheiten

Tuple-Structs sind nominal, anonyme Tupel strukturell.

Punkt(1.0, 2.0) und (1.0, 2.0) haben identisches Memory-Layout, aber unterschiedliche Typen. Funktionen, die Punkt erwarten, lehnen (f64, f64) ab. Das ist die Stärke der Tuple-Structs — eigener Typ mit Typsicherheit.

Newtype mit einem Feld ist ein Standard-Rust-Pattern.

struct Meter(f64), struct UserId(u64), struct ApiToken(String) — überall in idiomatischem Rust. Macht aus einem primitiven Typ einen domain-spezifischen Typ mit Typsicherheit.

Bei 4+ Feldern ist ein Field-Struct meist besser.

PersonTuple(name, alter, aktiv, email, ...) wird schnell unleserlich — was war .3 nochmal? Field-Struct mit benannten Feldern ist klarer. Tuple-Structs leuchten bei 1-3 positionalen Feldern mit klarer Konvention (RGB, IPv4, x/y).

Field-Access via .0/.1, nicht via Variable.

let i = 0; let v = p.i; funktioniert NICHT — Indices müssen literale Konstanten sein. Wer dynamischen Index braucht: nicht Tuple-Struct, sondern Array oder Vec.

Tuple-Struct als Konstruktor-Funktion verwendbar.

Punkt ist auch eine Funktion: Punkt(1.0, 2.0) ruft die implizite Konstruktor-Funktion. Du kannst sie sogar als Closure übergeben: (0..5).map(|i| Punkt(i as f64, 0.0)).

derive funktioniert auf Tuple-Structs wie auf Field-Structs.

#[derive(Debug, Clone, PartialEq)] auf Punkt(f64, f64) macht die Felder über Debug/Clone/etc. zugänglich. Voraussetzung: alle Felder selbst implementieren die jeweiligen Traits.

Newtype über Trait-Crates: nutze sie für Custom-Impls.

Wer einen eigenen Trait-Impl für Vec<T> braucht, hat ein Problem: die Orphan-Rule verhindert das (sowohl Vec als auch der externe Trait sind nicht „deiner"). Lösung: Newtype-Wrapper struct MyVec(Vec<T>) — auf dem darfst du jeden Trait implementieren.

repr(transparent) für Zero-Cost-Newtype.

#[repr(transparent)] struct Meter(f64); garantiert, dass Meter im Speicher identisch zu f64 ist. Wichtig bei FFI oder wenn die Newtype-Hülle zur Laufzeit keine Kosten verursachen darf.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Structs & Methoden

Zur Übersicht