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)
}

Drei Formen pro Variante:

  • Unit — keine Daten. Message::Ping.
  • Tuple — positionale Felder. Message::Text(String::from("Hi")).
  • Struct — benannte Felder. Message::Click { x: 10, y: 20 }.

Du kannst alle drei Formen in einem Enum mischen, je nachdem, welche Daten zur jeweiligen Variante passen.

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})"),
    }
}

Jeder Arm matcht eine Variante und bindet die enthaltenen Daten an Namen. Die exhaustive-Prüfung des Compilers garantiert, dass du keine vergisst.

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 sehr häufig. Sie kapseln Verhalten basierend auf der Variante. Ein klassisches Visitor-Pattern — aber ohne Boilerplate-Code.

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 das Enum:

  • Diskriminante (1 Byte für kleine Enums).
  • Plus den Platz der größten Variante.
  • Plus ggf. Padding für Alignment.
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!)
}

Achtung: u8 reicht für 200, 404, 500 nicht aus (über 255). #[repr(u16)] würde passen. Für Discriminant-Casts brauchst du den richtigen repr-Typ.

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
}

Bei &i32: der Null-Pointer ist nicht legal, weil Referenzen niemals null sein dürfen. Der Compiler nutzt das 0x0000_0000_0000_0000-Pattern als None-Marker. Option<&i32> braucht keine zusätzliche Diskriminante.

Bei i32: alle 4 Milliarden Pattern sind legal. Der Compiler braucht eine separate Diskriminante (1 Byte plus 3 Padding). Option<i32> ist daher 8 Bytes.

Diese Niche-Optimization macht Option<&T> so effizient wie ein nullable Pointer in C — aber typsicher.

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 { .. })
    }
}

Klassisches Domain-Modell: vier Antwort-Typen, jeweils mit relevanten Daten. Methoden für Common-Operations.

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
}

Token-Stream als Vec<Token> — jede Token-Art ist eine Variante.

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,
        }
    }
}

Klassische State-Machine: jeder State hat eigene Daten, Übergänge via weiter-Methode.

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}"),
    }
}

Heterogene Event-Sammlung. Alle Events durch ein Channel, einheitliche Verarbeitung im Receiver.

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