Ein Rust-Enum ist nicht nur eine Sammlung benannter Konstanten — er ist ein algebraischer Datentyp (Sum-Type), bei dem jede Variante eigene Daten in unterschiedlicher Form mitbringen kann. Damit modelliert ein Enum Konzepte, die in anderen Sprachen mehrere Klassen plus Vererbung bräuchten: HTTP-Responses (Erfolg mit Body oder Fehler mit Code), Token-Typen in einem Parser, Events in einem System, State-Machines. Dieser Artikel zeigt die Syntax für alle drei Varianten-Formen, die Methoden-Definition mit impl, das Speicher-Layout mit Diskriminanten und die Niche-Optimization, die Option<&T> so effizient macht.

Drei Varianten-Formen

Rust Drei Varianten-Stile
enum Message {
    Ping,                                 // Unit-Variante (keine Daten)
    Text(String),                          // Tuple-Variante (positional)
    Click { x: i32, y: i32 },              // Struct-Variante (benannt)
}

Rust kennt drei Formen für Enum-Varianten — und das schöne ist: du kannst sie frei mischen. In einem einzigen Enum dürfen Unit-, Tuple- und Struct-Varianten nebeneinander stehen, je nachdem, was zur jeweiligen Variante semantisch passt.

Unit-Varianten haben keine Daten — Message::Ping ist einfach ein Marker, der „hier passiert ein Ping" sagt. Sie sind das Pendant zu klassischen C-Enum-Konstanten.

Tuple-Varianten halten positionale Felder — Message::Text(String) enthält genau einen String, der namenlos in Position 0 sitzt. Idiomatisch, wenn die Variante ein bis drei semantisch klare Werte mitbringt.

Struct-Varianten halten benannte Felder — Message::Click { x, y } macht klar, welcher Wert was bedeutet. Sinnvoll bei mehreren Feldern oder wenn die Bedeutung aus der Position nicht ersichtlich wäre.

Die Mischform ist das Mächtige: ein einziger Message-Typ kann sowohl trockene Marker als auch reichhaltige Strukturen halten, jeweils in der Form, die zum Inhalt passt.

Konstruktion

Rust Varianten erstellen
# enum Message { Ping, Text(String), Click { x: i32, y: i32 } }
let m1 = Message::Ping;
let m2 = Message::Text(String::from("Hallo"));
let m3 = Message::Click { x: 10, y: 20 };

Jede Variante wird mit dem Enum-Namen plus :: plus Varianten-Namen erstellt. Bei Tuple-/Struct-Varianten kommen die Daten in der entsprechenden Form.

Zugriff auf Varianten-Daten

Du kannst auf die inneren Daten nicht direkt zugreifen — das ginge ja nur, wenn du wüsstest, welche Variante aktiv ist. Stattdessen Pattern-Matching:

Rust match
# enum Message { Ping, Text(String), Click { x: i32, y: i32 } }
fn beschreibe(m: &Message) -> String {
    match m {
        Message::Ping => "Ping".to_string(),
        Message::Text(s) => format!("Text: {s}"),
        Message::Click { x, y } => format!("Klick bei ({x}, {y})"),
    }
}

Auf die inneren Daten einer Enum-Variante kannst du nicht direkt zugreifen — das wäre nur möglich, wenn du wüsstest, welche Variante gerade aktiv ist. Aber genau das ist zur Compile-Zeit unbekannt: zur Laufzeit könnte m jede der drei Varianten sein. Statt riskanter Annahmen verlangt Rust Pattern-Matching als sicheren Zugriffsweg.

Jeder Arm matcht eine Variante und bindet die enthaltenen Daten an Namen — s für den Text-String, x und y für die Click-Koordinaten. Erst innerhalb des passenden Arms hast du Zugriff auf die Daten, und der Compiler weiß sicher, dass sie da sind. Die exhaustive-Prüfung des Compilers garantiert zusätzlich, dass du keine Variante vergisst — wenn das Enum später erweitert wird, weisst dich der Compiler überall auf die fehlende Behandlung hin.

Methoden mit impl

Wie bei Structs definierst du Methoden in einem impl-Block:

Rust impl auf Enum
enum Form {
    Kreis(f64),                            // Radius
    Rechteck { breite: f64, hoehe: f64 },
    Dreieck(f64, f64, f64),                // drei Seiten
}

impl Form {
    fn flaeche(&self) -> f64 {
        match self {
            Form::Kreis(r) => std::f64::consts::PI * r * r,
            Form::Rechteck { breite, hoehe } => breite * hoehe,
            Form::Dreieck(a, b, c) => {
                // Heron-Formel
                let s = (a + b + c) / 2.0;
                (s * (s-a) * (s-b) * (s-c)).sqrt()
            }
        }
    }

    fn ist_konvex(&self) -> bool {
        true        // alle hier sind konvex
    }
}

fn main() {
    let k = Form::Kreis(5.0);
    println!("{:.2}", k.flaeche());      // 78.54
}

Methoden auf Enums sind genauso gängig wie auf Structs. Der Unterschied liegt im Methoden-Body: typischerweise gibt es ein match self, das pro Variante anders verfährt. Die flaeche-Methode zeigt das paradigmatisch — für jeden Form-Typ wird die passende Geometrie-Formel angewendet.

Das ist das Visitor-Pattern aus der OOP-Welt, nur ohne den dortigen Boilerplate-Code. In Java oder C++ bräuchtest du eine Hierarchie aus abstrakter Form-Klasse plus konkreten Subklassen plus Visitor-Interface plus konkretem Visitor-Impl, um genau dieses Verhalten zu erreichen. Mit Rust-Enums ist es ein einziger Block. Und die Exhaustiveness-Garantie sorgt dafür, dass du beim Hinzufügen einer neuen Form-Variante (etwa Form::Sechseck) automatisch in jeder Methode darauf hingewiesen wirst, sie zu behandeln.

Diskriminante: das Speicher-Layout

Jede Variante eines Enums hat eine Diskriminante — eine kleine Zahl, die zur Laufzeit identifiziert, welche Variante aktiv ist:

Rust Diskriminante
enum Status {
    Aktiv,           // Diskriminante 0
    Inaktiv,         // Diskriminante 1
    Geloescht,       // Diskriminante 2
}

Der Compiler wählt die Diskriminante-Größe automatisch — typischerweise u8, manchmal größer.

Im Speicher belegt ein Enum so viel Platz, wie er für alle möglichen Varianten brauchen würde — plus etwas Overhead für die Variant-Identifikation. Konkret setzt sich das Speicher-Layout aus drei Komponenten zusammen:

Die Diskriminante ist die kleine Zahl, die zur Laufzeit angibt, welche Variante gerade aktiv ist. Für kleine Enums (bis 256 Varianten) reicht 1 Byte; bei mehr Varianten wählt der Compiler automatisch größer.

Den Platz der größten Variante — denn das Enum muss jeden möglichen Inhalt aufnehmen können. Bei enum { A, B(u32), C(u64) } ist die größte Variante C(u64) mit 8 Bytes; das gesamte Enum braucht entsprechend mindestens 8 Bytes Daten-Speicher, auch wenn gerade die kleine A-Variante aktiv ist.

Padding für Alignment-Anforderungen. Wenn die Daten-Variante eine 8-Byte-Alignment-Anforderung hat (u64), muss die Diskriminante so platziert werden, dass die Daten korrekt aligned beginnen — was zusätzliche Padding-Bytes bedeutet.

Rust size_of
use std::mem::size_of;

enum E {
    A,                  // 0 Bytes Daten
    B(u32),             // 4 Bytes Daten
    C(u64),             // 8 Bytes Daten
}

fn main() {
    println!("{}", size_of::<E>());   // 16 (u64-Daten + Tag + Padding)
}

Manuelle Diskriminante

Du kannst die Diskriminante explizit setzen — nützlich für FFI mit C oder für Wire-Formate:

Rust Explizite Werte
#[repr(u8)]
enum HttpStatus {
    Ok = 200,
    NotFound = 404,
    InternalError = 500,
}

fn main() {
    let s = HttpStatus::NotFound;
    let code = s as u8;
    println!("{code}");          // 404 (... naja, 148 — u8 wrappt bei 255!)
}

Mit #[repr(typ)] kannst du die Diskriminante explizit setzen und ihre Repräsentation kontrollieren. Das ist wichtig für FFI mit C (wo die Diskriminante einem definierten Integer-Wert entsprechen muss) und für Wire-Formate (Protokolle, Disk-Formate), wo die numerischen Werte stabil sein müssen.

Achtung beim Beispiel: u8 reicht für die Werte 200, 404, 500 nicht aus — u8 deckt nur 0-255 ab. 404 as u8 ergibt 148 (Wrap-Around). Für HTTP-Status wäre #[repr(u16)] die korrekte Wahl. Solche Tücken treten bei Discriminant-Casts immer wieder auf — wähle den repr-Typ groß genug für deine größte Variante.

Niche-Optimization

Wenn ein Typ nicht alle Bit-Pattern legal nutzt, kann der Compiler die ungenutzten Pattern als Diskriminante recyceln:

Rust Niche
use std::mem::size_of;

fn main() {
    println!("{}", size_of::<&i32>());                  // 8
    println!("{}", size_of::<Option<&i32>>());          // 8 — gleich!
    println!("{}", size_of::<i32>());                   // 4
    println!("{}", size_of::<Option<i32>>());           // 8 — größer
}

Hier siehst du eine der elegantesten Compiler-Optimierungen in Rust. Bei Option<&i32> ist die Größe 8 Bytes — genau wie ein reiner &i32. Das ist überraschend: normalerweise würde man erwarten, dass Option eine zusätzliche Diskriminante (1 Byte) plus Padding (7 Bytes für Alignment) braucht, also 16 Bytes insgesamt.

Der Trick: bei &i32 ist der Null-Pointer ein verbotenes Bit-Pattern, weil Rust-Referenzen niemals null sein dürfen. Der Compiler erkennt das und nutzt das ungenutzte 0x0000_0000_0000_0000-Pattern als None-Marker. Damit braucht Option<&i32> keine zusätzliche Diskriminante — die Information „ist es None?" steckt direkt in den Pointer-Bits.

Bei Option<i32> greift das nicht: für i32 sind alle 4 Milliarden Bit-Patterns legal, es gibt keinen freien Slot. Der Compiler muss eine separate Diskriminante anhängen, daher 8 Bytes statt 4.

Diese Niche-Optimization macht Option<&T>, Option<Box<T>>, Option<NonZeroU32> und viele andere Wrapper so effizient wie ein nullable Pointer in C — aber mit voller Typ-Sicherheit. Du hast die null-Funktionalität ohne den null-Bug-Albtraum.

Derive-Traits auf Enums

Wie Structs unterstützen Enums das #[derive]-System:

Rust Standard-Derives
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Rolle {
    Admin,
    User,
    Gast,
}

Voraussetzung: alle Daten in allen Varianten implementieren den jeweiligen Trait selbst.

Copy ist möglich, wenn alle Varianten-Daten Copy sind:

Rust Copy-Enum
#[derive(Debug, Clone, Copy)]
enum Ampel { Rot, Gelb, Gruen }       // Unit-Varianten → trivial Copy

Generische Enums

Enums können wie Structs Type-Parameter haben:

Rust Generisches Enum
enum Resultat<T, E> {
    Erfolg(T),
    Fehler(E),
}

impl<T, E> Resultat<T, E> {
    fn ist_erfolg(&self) -> bool {
        matches!(self, Resultat::Erfolg(_))
    }
}

fn main() {
    let r: Resultat<i32, String> = Resultat::Erfolg(42);
    assert!(r.ist_erfolg());
}

Das Stdlib-Result<T, E> ist exakt diese Form. Ebenso Option<T> mit einem Type-Parameter.

Praxis: Enums im echten Code

HTTP-Response-Modell

Rust HTTP-Response
pub enum HttpResponse {
    Ok { body: Vec<u8> },
    Redirect { ziel: String, status: u16 },
    ClientError { code: u16, nachricht: String },
    ServerError { code: u16, details: Option<String> },
}

impl HttpResponse {
    pub fn status_code(&self) -> u16 {
        match self {
            HttpResponse::Ok { .. } => 200,
            HttpResponse::Redirect { status, .. } => *status,
            HttpResponse::ClientError { code, .. } => *code,
            HttpResponse::ServerError { code, .. } => *code,
        }
    }

    pub fn ist_erfolg(&self) -> bool {
        matches!(self, HttpResponse::Ok { .. })
    }
}

Ein klassisches Domain-Modell für HTTP-Responses. Jede Variante hat genau die Daten, die für ihre Art relevant sind: Ok braucht nur einen Body, Redirect braucht Ziel und Status, ClientError und ServerError haben Code plus Beschreibung. Ein einziges Enum modelliert das gesamte HTTP-Response-Spektrum.

Die status_code-Methode zeigt, wie Pattern-Matching mit Daten-Extraktion funktioniert: Ok { .. } ignoriert die Daten und liefert konstant 200; Redirect { status, .. } bindet das status-Feld und gibt es zurück. Die ..-Syntax ist hier wichtig: sie sagt „alle anderen Felder interessieren mich nicht". Ohne .. müssten alle Felder explizit benannt oder mit _ markiert werden.

Token-Typ in einem Parser

Rust Tokenizer
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    Zahl(i64),
    Bezeichner(String),
    String(String),
    Plus,
    Minus,
    Mal,
    Geteilt,
    KlammerAuf,
    KlammerZu,
    Eof,
}

fn tokenize(input: &str) -> Vec<Token> {
    let mut tokens = Vec::new();
    // ... vereinfacht
    for ch in input.chars() {
        match ch {
            '+' => tokens.push(Token::Plus),
            '-' => tokens.push(Token::Minus),
            _ => {}
        }
    }
    tokens.push(Token::Eof);
    tokens
}

In einem Parser ist das Token-Enum die zentrale Datenstruktur. Jede syntaktische Einheit (Zahl, Bezeichner, String-Literal, Operator, Klammer) bekommt eine eigene Variante. Manche tragen Daten (Zahl(i64), Bezeichner(String)), andere sind reine Marker (Plus, Minus, KlammerAuf).

Das Eof-Token am Ende ist eine bewährte Konvention: es signalisiert „Eingabe komplett verarbeitet". Damit kann der Parser einheitlich gegen Tokens matchen, ohne separate „ende erreicht?"-Checks. Diese Art von Token-Enum ist die Grundlage praktisch jedes Compilers, Interpreters, Konfigurations-Parsers in Rust — von kleinen Skript-Engines bis zu großen Sprach-Implementierungen wie rustc selbst.

State-Machine

Rust Verbindungs-Lifecycle
pub enum VerbindungsState {
    Inaktiv,
    Verbinde { ziel: String, versuche: u32 },
    Verbunden { socket_id: u64, peer: String },
    Geschlossen { grund: String },
}

impl VerbindungsState {
    pub fn weiter(self) -> VerbindungsState {
        match self {
            VerbindungsState::Inaktiv => {
                VerbindungsState::Verbinde {
                    ziel: "example.com".into(),
                    versuche: 1,
                }
            }
            VerbindungsState::Verbinde { ziel, versuche } if versuche < 3 => {
                VerbindungsState::Verbunden { socket_id: 42, peer: ziel }
            }
            VerbindungsState::Verbinde { .. } => {
                VerbindungsState::Geschlossen { grund: "Timeout".into() }
            }
            weiter @ _ => weiter,
        }
    }
}

State-Machines mit Enums sind ein Kern-Idiom in Rust. Jede Variante repräsentiert einen State und hält die für diesen State relevanten Daten — der Verbinde-State braucht Ziel und Versuche-Counter, der Verbunden-State braucht Socket-ID und Peer-Info. So sind Daten und State untrennbar verbunden.

Die weiter-Methode nimmt self (konsumierend) und gibt einen neuen VerbindungsState zurück — der alte State wird ersetzt. Das ist die idiomatische Form für State-Transitions: das Enum wird per Move durchgereicht, die Übergangs-Logik passiert im match-Block. Da self konsumiert wird, gibt es keinen Inkonsistenz-Zustand zwischen alt und neu.

Eine subtile Detail: das weiter @ _-Pattern am Ende fängt alle nicht-vorher-behandelten Varianten ab und gibt sie unverändert zurück. Das @-Binding bindet die gematchte Variante an den Namen weiter, der dann als Rückgabewert dient.

Event-System

Rust Events
pub enum Event {
    UserLoggedIn { user_id: u64, timestamp: u64 },
    UserLoggedOut { user_id: u64 },
    OrderPlaced { order_id: u64, betrag_cent: i64 },
    OrderCancelled { order_id: u64, grund: String },
    PaymentReceived { order_id: u64, betrag_cent: i64 },
}

pub fn log_event(e: &Event) {
    match e {
        Event::UserLoggedIn { user_id, timestamp } =>
            println!("[{timestamp}] User {user_id} login"),
        Event::UserLoggedOut { user_id } =>
            println!("User {user_id} logout"),
        Event::OrderPlaced { order_id, betrag_cent } =>
            println!("Order {order_id}: {betrag_cent} cent"),
        Event::OrderCancelled { order_id, grund } =>
            println!("Order {order_id} cancelled: {grund}"),
        Event::PaymentReceived { order_id, betrag_cent } =>
            println!("Payment {betrag_cent} für Order {order_id}"),
    }
}

Event-basierte Systeme nutzen Enums als zentrale Event-Repräsentation. Jede Event-Art bekommt eine Variante mit ihren spezifischen Daten; der Logger oder Event-Handler erkennt die Variante per match und verarbeitet sie passend.

Der Vorteil dieser Variante: ein einziger Channel-Typ kann alle Events transportieren. Statt für jede Event-Art einen eigenen Sender zu haben, gibt es einen Sender<Event>, der jede Variante akzeptiert. Im Receiver-Thread läuft ein einziger match, der für jede Event-Art die richtige Logik wählt. Diese Architektur ist in vielen Spiel-Engines, GUI-Frameworks und Backend-Services zu finden.

Konfigurations-Werte mit verschiedenen Typen

Rust JSON-artiger Wert
#[derive(Debug, Clone)]
pub enum ConfigValue {
    String(String),
    Integer(i64),
    Float(f64),
    Bool(bool),
    Array(Vec<ConfigValue>),
    Null,
}

impl ConfigValue {
    pub fn als_string(&self) -> Option<&str> {
        if let ConfigValue::String(s) = self { Some(s) } else { None }
    }

    pub fn als_int(&self) -> Option<i64> {
        if let ConfigValue::Integer(i) = self { Some(*i) } else { None }
    }
}

Klassisches JSON-Wert-Modell. Rekursiv (Array(Vec<ConfigValue>)), beliebig tief verschachtelbar.

Command-Pattern

Rust Commands
pub enum Befehl {
    Lesen { pfad: String },
    Schreiben { pfad: String, daten: Vec<u8> },
    Loeschen { pfad: String },
    Verschieben { von: String, nach: String },
}

pub fn ausfuehren(b: Befehl) -> Result<String, String> {
    match b {
        Befehl::Lesen { pfad } => Ok(format!("Inhalt von {pfad}")),
        Befehl::Schreiben { pfad, daten } => {
            Ok(format!("{} Bytes nach {pfad} geschrieben", daten.len()))
        }
        Befehl::Loeschen { pfad } => Ok(format!("{pfad} gelöscht")),
        Befehl::Verschieben { von, nach } => Ok(format!("{von} → {nach}")),
    }
}

Klassische Anwendung von Sum-Types: ein Daten-Typ für viele Operations-Arten, dispatched über match.

Fehler-Hierarchie

Rust Application-Errors
#[derive(Debug)]
pub enum AppError {
    NetzwerkFehler { code: u32, nachricht: String },
    ParseError(String),
    NichtGefunden { ressource: String },
    UngueltigeEingabe { feld: String, grund: String },
    Datenbank(String),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::NetzwerkFehler { code, nachricht } =>
                write!(f, "Netzwerk {code}: {nachricht}"),
            AppError::ParseError(s) => write!(f, "Parse: {s}"),
            AppError::NichtGefunden { ressource } =>
                write!(f, "Nicht gefunden: {ressource}"),
            AppError::UngueltigeEingabe { feld, grund } =>
                write!(f, "Ungültiges {feld}: {grund}"),
            AppError::Datenbank(s) => write!(f, "DB: {s}"),
        }
    }
}

impl std::error::Error for AppError {}

Domain-spezifische Error-Hierarchie. Mit thiserror-Crate noch kompakter — aber die manuelle Form zeigt, was passiert.

Generisch über Typ-Parameter

Rust Eigenes Result
pub enum ProcessResult<T> {
    Erfolg(T),
    TemporaerFehler { retry_nach_sec: u32, grund: String },
    DauerhafterFehler(String),
}

impl<T> ProcessResult<T> {
    pub fn ist_retry_moeglich(&self) -> bool {
        matches!(self, ProcessResult::TemporaerFehler { .. })
    }
}

Generischer Result-Typ mit zusätzlicher Information (Retry-Logik).

Interessantes

Enum-Varianten können beliebige Daten halten.

Anders als in C, wo Enums nur Integer sind. In Rust trägt jede Variante ihre eigenen Daten — primitiv, Struct-artig, Tuple-artig. Das macht Enums zu echten algebraischen Datentypen.

Methoden via impl wie bei Structs.

impl Enum { fn methode(&self) { ... } } funktioniert genau wie bei Structs. Methoden machen typisch ein match auf self, um Varianten-spezifisches Verhalten zu implementieren.

Diskriminante ist meistens implizit.

Der Compiler wählt eine kleine Diskriminante (oft u8) und legt sie ins Memory-Layout. Bei expliziter Wahl: #[repr(u32)] enum E { ... }. Wichtig nur bei FFI mit C oder bei Wire-Formaten.

Niche-Optimization spart Speicher.

Option<&T> ist genauso groß wie &T, weil der Compiler den Null-Pointer als None recyceln kann. Option<bool> ist 1 Byte, weil bool nur 2 von 256 Pattern legal nutzt. Auch Option<NonZeroU32> ist 4 Bytes — der 0-Wert dient als None-Marker.

matches!-Macro für boolesche Variant-Checks.

matches!(value, Pattern) ist syntaktischer Zucker für if let Pattern = value { true } else { false }. Praktisch bei is_X-Methoden: pub fn ist_fehler(&self) -> bool { matches!(self, Result::Err(_)) }.

#[non_exhaustive] für vorwärtskompatible Public-APIs.

Wer ein Enum in einem Library-Crate als #[non_exhaustive] markiert, sagt Konsumenten: „dieses Enum kann in Zukunft neue Varianten bekommen". Match-Statements im Konsumenten brauchen dann immer einen _-Branch — sonst Compile-Fehler bei jeder neuen Variante.

Enums sind keine Klassen mit Vererbung.

Rust hat keine OOP-Vererbung. Wo Java „Animal mit Subklassen Dog/Cat" sagt, sagt Rust enum Animal { Dog(Dog), Cat(Cat) } oder nutzt trait Animal mit impl Animal for Dog. Beide Ansätze haben ihren Platz — siehe Traits-Kapitel.

Generische Enums sind sehr mächtig.

Die Stdlib hat Option<T> und Result<T, E> als generische Enums. Deine eigenen Enums können das auch — enum Cache<T> { Hit(T), Miss } oder enum Either<L, R> { Left(L), Right(R) }. Sehr flexibel für API-Design.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Enums & Pattern Matching

Zur Übersicht