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)
}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
# 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})"),
}
}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:
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:
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.
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!)
}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:
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:
#[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 { .. })
}
}Klassisches Domain-Modell: vier Antwort-Typen, jeweils mit relevanten Daten. Methoden für Common-Operations.
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
}Token-Stream als Vec<Token> — jede Token-Art ist eine Variante.
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,
}
}
}Klassische State-Machine: jeder State hat eigene Daten, Übergänge via weiter-Methode.
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}"),
}
}Heterogene Event-Sammlung. Alle Events durch ein Channel, einheitliche Verarbeitung im Receiver.
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.