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
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
# 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:
# 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:
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:
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.
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:
#[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:
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:
#[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:
#[derive(Debug, Clone, Copy)]
enum Ampel { Rot, Gelb, Gruen } // Unit-Varianten → trivial CopyGenerische Enums
Enums können wie Structs Type-Parameter haben:
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
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
#[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
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
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
#[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
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
#[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
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.